mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-01 23:53:20 +03:00
Compare commits
135 Commits
1.4.5
...
28cca601b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28cca601b5 | ||
|
|
93cfd56954 | ||
|
|
5e7484c51b | ||
|
|
43df9fb7a1 | ||
|
|
8d65f21f23 | ||
|
|
91ebbbd31d | ||
|
|
a965e8cf8f | ||
|
|
c737611538 | ||
|
|
9f2ce33a6c | ||
|
|
27c0cd4f9b | ||
|
|
6f590a07b5 | ||
|
|
51b562c6c8 | ||
|
|
f02cd9c0f6 | ||
|
|
170516572e | ||
|
|
285e29d2dc | ||
|
|
aab34b2338 | ||
|
|
ad1e5330e9 | ||
|
|
ca4647ddd6 | ||
|
|
7004acae46 | ||
|
|
899dd46f5b | ||
|
|
dba5fea66f | ||
|
|
c457b0e7d3 | ||
|
|
c0da4a6645 | ||
|
|
9d8df6a226 | ||
|
|
02da7132e7 | ||
|
|
e3b6e4eaf0 | ||
|
|
0388d00ad3 | ||
|
|
1e2d2c5146 | ||
|
|
96797742f2 | ||
|
|
682e347be0 | ||
|
|
b3f43f55c1 | ||
|
|
016a0b1141 | ||
|
|
fd7bcf54bd | ||
|
|
db3f5fe816 | ||
|
|
0d3016fcd8 | ||
|
|
1abc897c45 | ||
|
|
ab64a32f30 | ||
|
|
52b66e71d1 | ||
|
|
41ab5bbdd8 | ||
|
|
732b250815 | ||
|
|
157dbdc543 | ||
|
|
6ba23683d5 | ||
|
|
80a5865db3 | ||
|
|
9cb6f38aea | ||
|
|
cd7e3e4505 | ||
|
|
1833cb0655 | ||
|
|
e4208aa9cf | ||
|
|
bb3501a4f9 | ||
|
|
4abdb2e08b | ||
|
|
d49ae493b2 | ||
|
|
394079833e | ||
|
|
12d6789c2e | ||
|
|
34803f8e9b | ||
|
|
fd43184406 | ||
|
|
3cc3315081 | ||
|
|
6aee70fa18 | ||
|
|
82a9fd1540 | ||
|
|
dc760d6ca8 | ||
|
|
eb239501bc | ||
|
|
0016033937 | ||
|
|
50c62d5eac | ||
|
|
91ac48912e | ||
|
|
272a6604cd | ||
|
|
8a889d3ebb | ||
|
|
17a3f2ae52 | ||
|
|
6c3515588f | ||
|
|
4d2d2118a2 | ||
|
|
483fe80308 | ||
|
|
34ceeac36e | ||
|
|
20f11018ce | ||
|
|
9345fb754a | ||
|
|
779b7aaf02 | ||
|
|
b268aa1061 | ||
|
|
40f86fa639 | ||
|
|
980bc11e68 | ||
|
|
85db677982 | ||
|
|
2842315b1d | ||
|
|
6c541f7bfd | ||
|
|
067fab2b73 | ||
|
|
de6bf9dc7e | ||
|
|
54eae37038 | ||
|
|
0118e16132 | ||
|
|
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 |
@@ -1,56 +0,0 @@
|
||||
You are an expert in prompt engineering, specializing in optimizing AI code assistant instructions. Your task is to analyze and improve the instructions for Claude Code.
|
||||
Follow these steps carefully:
|
||||
|
||||
1. Analysis Phase:
|
||||
Review the chat history in your context window.
|
||||
|
||||
Then, examine the current Claude instructions, commands and config
|
||||
<claude_instructions>
|
||||
/CLAUDE.md
|
||||
/.claude/commands/*
|
||||
**/CLAUDE.md
|
||||
.claude/settings.json
|
||||
.claude/settings.local.json
|
||||
</claude_instructions>
|
||||
|
||||
Analyze the chat history, instructions, commands and config to identify areas that could be improved. Look for:
|
||||
- Inconsistencies in Claude's responses
|
||||
- Misunderstandings of user requests
|
||||
- Areas where Claude could provide more detailed or accurate information
|
||||
- Opportunities to enhance Claude's ability to handle specific types of queries or tasks
|
||||
- New commands or improvements to a commands name, function or response
|
||||
- Permissions and MCPs we've approved locally that we should add to the config, especially if we've added new tools or require them for the command to work
|
||||
|
||||
2. Interaction Phase:
|
||||
Present your findings and improvement ideas to the human. For each suggestion:
|
||||
a) Explain the current issue you've identified
|
||||
b) Propose a specific change or addition to the instructions
|
||||
c) Describe how this change would improve Claude's performance
|
||||
|
||||
Wait for feedback from the human on each suggestion before proceeding. If the human approves a change, move it to the implementation phase. If not, refine your suggestion or move on to the next idea.
|
||||
|
||||
3. Implementation Phase:
|
||||
For each approved change:
|
||||
a) Clearly state the section of the instructions you're modifying
|
||||
b) Present the new or modified text for that section
|
||||
c) Explain how this change addresses the issue identified in the analysis phase
|
||||
|
||||
4. Output Format:
|
||||
Present your final output in the following structure:
|
||||
|
||||
<analysis>
|
||||
[List the issues identified and potential improvements]
|
||||
</analysis>
|
||||
|
||||
<improvements>
|
||||
[For each approved improvement:
|
||||
1. Section being modified
|
||||
2. New or modified instruction text
|
||||
3. Explanation of how this addresses the identified issue]
|
||||
</improvements>
|
||||
|
||||
<final_instructions>
|
||||
[Present the complete, updated set of instructions for Claude, incorporating all approved changes]
|
||||
</final_instructions>
|
||||
|
||||
Remember, your goal is to enhance Claude's performance and consistency while maintaining the core functionality and purpose of the AI assistant. Be thorough in your analysis, clear in your explanations, and precise in your implementations.
|
||||
12
.github/workflows/flutter-build.yml
vendored
12
.github/workflows/flutter-build.yml
vendored
@@ -39,8 +39,8 @@ 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.5"
|
||||
NDK_VERSION: "r27c"
|
||||
VERSION: "1.4.6"
|
||||
NDK_VERSION: "r28c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||
@@ -234,7 +234,7 @@ 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
|
||||
@@ -266,7 +266,7 @@ 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=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
|
||||
@@ -400,7 +400,7 @@ 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
|
||||
@@ -418,7 +418,7 @@ 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=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
|
||||
|
||||
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.5"
|
||||
VERSION: "1.4.6"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
15
.github/workflows/winget.yml
vendored
15
.github/workflows/winget.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Publish to WinGet
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: "1.4.5"
|
||||
release-tag: "1.4.5"
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
.env
|
||||
libsciter-gtk.so
|
||||
src/ui/inline.rs
|
||||
extractor
|
||||
|
||||
368
Cargo.lock
generated
368
Cargo.lock
generated
@@ -33,6 +33,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
@@ -293,8 +299,8 @@ dependencies = [
|
||||
"image 0.25.1",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde 1.0.228",
|
||||
@@ -637,7 +643,7 @@ dependencies = [
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.7.4",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
@@ -860,6 +866,15 @@ dependencies = [
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.1"
|
||||
@@ -1182,7 +1197,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"num-traits 0.2.19",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1290,8 +1305,8 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
@@ -2216,6 +2231,15 @@ dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
@@ -2233,7 +2257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"redox_users 0.4.5",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
@@ -2245,10 +2269,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"redox_users 0.4.5",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
@@ -2256,7 +2292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"redox_users 0.4.5",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
@@ -2266,6 +2302,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@@ -2517,6 +2563,7 @@ version = "0.0.14"
|
||||
dependencies = [
|
||||
"core-graphics 0.22.3",
|
||||
"hbb_common",
|
||||
"libxdo-sys",
|
||||
"log",
|
||||
"objc",
|
||||
"pkg-config",
|
||||
@@ -2714,7 +2761,7 @@ dependencies = [
|
||||
"flume",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.7.4",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
@@ -2800,12 +2847,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.30"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3720,6 +3767,7 @@ dependencies = [
|
||||
"httparse",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"libloading 0.8.4",
|
||||
"log",
|
||||
"mac_address",
|
||||
"machine-uid",
|
||||
@@ -3755,6 +3803,7 @@ dependencies = [
|
||||
"webrtc",
|
||||
"whoami",
|
||||
"winapi 0.3.9",
|
||||
"x11 2.21.0",
|
||||
"zstd 0.13.1",
|
||||
]
|
||||
|
||||
@@ -4038,7 +4087,7 @@ dependencies = [
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-traits 0.2.19",
|
||||
"png",
|
||||
"png 0.17.13",
|
||||
"qoi",
|
||||
"tiff",
|
||||
]
|
||||
@@ -4052,7 +4101,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"num-traits 0.2.19",
|
||||
"png",
|
||||
"png 0.17.13",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
@@ -4546,11 +4595,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libxdo-sys"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"x11 2.21.0",
|
||||
"hbb_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4766,6 +4812,16 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
@@ -4816,21 +4872,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.13.5"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
|
||||
checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
|
||||
dependencies = [
|
||||
"cocoa 0.25.0",
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
"gtk",
|
||||
"keyboard-types",
|
||||
"libxdo",
|
||||
"objc",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"once_cell",
|
||||
"png",
|
||||
"thiserror 1.0.61",
|
||||
"windows-sys 0.52.0",
|
||||
"png 0.17.13",
|
||||
"thiserror 2.0.17",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5374,7 +5432,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
|
||||
dependencies = [
|
||||
"objc-sys 0.3.5",
|
||||
"objc2-encode 4.0.3",
|
||||
"objc2-encode 4.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode 4.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5389,10 +5456,22 @@ dependencies = [
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-data",
|
||||
"objc2-core-image",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-quartz-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-cloud-kit"
|
||||
version = "0.2.2"
|
||||
@@ -5403,7 +5482,7 @@ dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5414,7 +5493,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5426,7 +5505,28 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"dispatch2",
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5437,7 +5537,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
]
|
||||
|
||||
@@ -5450,7 +5550,7 @@ dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-contacts",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5464,9 +5564,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.0.3"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
@@ -5481,6 +5581,18 @@ dependencies = [
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"block2 0.6.2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-link-presentation"
|
||||
version = "0.2.2"
|
||||
@@ -5489,8 +5601,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5502,7 +5614,7 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5514,7 +5626,7 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal",
|
||||
]
|
||||
|
||||
@@ -5525,7 +5637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
|
||||
dependencies = [
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5541,7 +5653,7 @@ dependencies = [
|
||||
"objc2-core-data",
|
||||
"objc2-core-image",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-link-presentation",
|
||||
"objc2-quartz-core",
|
||||
"objc2-symbols",
|
||||
@@ -5557,7 +5669,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5570,7 +5682,7 @@ dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6178,7 +6290,20 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6863,6 +6988,17 @@ dependencies = [
|
||||
"thiserror 1.0.61",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"libredox",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@@ -7134,7 +7270,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -7181,9 +7317,9 @@ dependencies = [
|
||||
"kcp-sys",
|
||||
"keepawake",
|
||||
"lazy_static",
|
||||
"libloading 0.8.4",
|
||||
"libpulse-binding",
|
||||
"libpulse-simple-binding",
|
||||
"libxdo-sys",
|
||||
"mac_address",
|
||||
"magnum-opus",
|
||||
"nix 0.29.0",
|
||||
@@ -7249,7 +7385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
@@ -7981,8 +8117,8 @@ dependencies = [
|
||||
"log",
|
||||
"memmap2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle 0.6.2",
|
||||
"redox_syscall 0.5.2",
|
||||
@@ -8312,7 +8448,7 @@ dependencies = [
|
||||
"objc",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"png",
|
||||
"png 0.17.13",
|
||||
"raw-window-handle 0.6.2",
|
||||
"scopeguard",
|
||||
"tao-macros",
|
||||
@@ -8566,7 +8702,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg-if 1.0.0",
|
||||
"log",
|
||||
"png",
|
||||
"png 0.17.13",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
@@ -8939,21 +9075,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tray-icon"
|
||||
version = "0.14.3"
|
||||
source = "git+https://github.com/tauri-apps/tray-icon#d4078696edba67b0ab42cef67e6a421a0332c96f"
|
||||
version = "0.21.3"
|
||||
source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6"
|
||||
dependencies = [
|
||||
"core-graphics 0.23.2",
|
||||
"crossbeam-channel",
|
||||
"dirs 5.0.1",
|
||||
"dirs 6.0.0",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.2",
|
||||
"once_cell",
|
||||
"png",
|
||||
"thiserror 1.0.61",
|
||||
"windows-sys 0.52.0",
|
||||
"png 0.18.1",
|
||||
"thiserror 2.0.17",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10058,7 +10195,7 @@ dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.0",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
@@ -10107,7 +10244,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.4.0",
|
||||
]
|
||||
@@ -10119,7 +10256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10172,6 +10309,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
@@ -10179,7 +10322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10197,7 +10340,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10217,7 +10360,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10226,7 +10369,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10256,6 +10399,24 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
@@ -10295,13 +10456,30 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.1"
|
||||
@@ -10338,6 +10516,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -10368,6 +10552,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.32.0"
|
||||
@@ -10398,12 +10588,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -10434,6 +10636,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.32.0"
|
||||
@@ -10464,6 +10672,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -10482,6 +10696,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.32.0"
|
||||
@@ -10512,6 +10732,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winit"
|
||||
version = "0.30.9"
|
||||
@@ -10536,8 +10762,8 @@ dependencies = [
|
||||
"memmap2",
|
||||
"ndk 0.9.0",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-ui-kit",
|
||||
"orbclient",
|
||||
"percent-encoding",
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
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"
|
||||
@@ -161,7 +160,7 @@ piet-coregraphics = "0.6"
|
||||
foreign-types = "0.3"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
|
||||
tray-icon = { git = "https://github.com/tauri-apps/tray-icon", version = "0.21.3" }
|
||||
tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" }
|
||||
image = "0.24"
|
||||
|
||||
@@ -177,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" }
|
||||
@@ -207,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.5
|
||||
version: 1.4.6
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.5
|
||||
version: 1.4.6
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
2
build.py
2
build.py
@@ -299,7 +299,7 @@ Version: %s
|
||||
Architecture: %s
|
||||
Maintainer: rustdesk <info@rustdesk.com>
|
||||
Homepage: https://rustdesk.com
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
||||
Recommends: libayatana-appindicator3-1
|
||||
Description: A remote control software.
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -167,7 +167,7 @@ target/release/rustdesk
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс на Sciter (устаревшее)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервисы аудио, буфера обмена, ввода, видео и сетевых подключений
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое соединение
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером Rustdesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript для Web-клиента Flutter
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
],
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=wayland",
|
||||
"--socket=x11",
|
||||
"--share=network",
|
||||
"--filesystem=home",
|
||||
|
||||
@@ -311,7 +311,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
||||
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
|
||||
}
|
||||
val idStopService = 2
|
||||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||
val hideStopService = FFI.getBuildinOption("hide-stop-service") == "Y"
|
||||
if (!hideStopService) {
|
||||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||
}
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
idShowRustDesk -> {
|
||||
@@ -389,4 +392,3 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ object FFI {
|
||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
external fun setCodecInfo(info: String)
|
||||
external fun getLocalOption(key: String): String
|
||||
external fun getBuildinOption(key: String): String
|
||||
external fun onClipboardUpdate(clips: ByteBuffer)
|
||||
external fun isServiceClipboardEnabled(): Boolean
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# 2024, Vasyl Gello <vasek.gello@gmail.com>
|
||||
#
|
||||
|
||||
# The script is invoked by F-Droid builder system ste-by-step.
|
||||
# The script is invoked by F-Droid builder system step-by-step.
|
||||
#
|
||||
# It accepts the following arguments:
|
||||
#
|
||||
@@ -16,7 +16,6 @@
|
||||
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
|
||||
# - The build step to execute:
|
||||
#
|
||||
# + sudo-deps: as root, install needed Debian packages into builder VM
|
||||
# + prebuild: patch sources and do other stuff before the build
|
||||
# + build: perform actual build of APK file
|
||||
#
|
||||
@@ -184,13 +183,9 @@ prebuild)
|
||||
fi
|
||||
|
||||
# Map NDK version to revision
|
||||
|
||||
NDK_VERSION="$(wget \
|
||||
-qO- \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
'https://api.github.com/repos/android/ndk/releases' |
|
||||
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||
NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
|
||||
jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
|
||||
sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
|
||||
|
||||
if [ -z "${NDK_VERSION}" ]; then
|
||||
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||
@@ -316,6 +311,18 @@ prebuild)
|
||||
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
|
||||
|
||||
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
|
||||
# Find first libclang.so and set BRIDGE_LLVM_PATH
|
||||
|
||||
BRIDGE_LLVM_PATH="$(find /usr/lib/ -name libclang.so | head -n1)"
|
||||
|
||||
if [ -z "${BRIDGE_LLVM_PATH}" ]; then
|
||||
echo 'ERROR: Can not find libclang.so for bridge generator!' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
|
||||
BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
|
||||
|
||||
# Install Flutter bridge version
|
||||
|
||||
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
|
||||
@@ -344,7 +351,8 @@ prebuild)
|
||||
|
||||
flutter_rust_bridge_codegen \
|
||||
--rust-input ./src/flutter_ffi.rs \
|
||||
--dart-output ./flutter/lib/generated_bridge.dart
|
||||
--dart-output ./flutter/lib/generated_bridge.dart \
|
||||
--llvm-path "${BRIDGE_LLVM_PATH}"
|
||||
|
||||
# Add bridge files to save-list
|
||||
|
||||
@@ -355,13 +363,15 @@ prebuild)
|
||||
git checkout '*'
|
||||
git clean -dffx
|
||||
git reset
|
||||
|
||||
unset BRIDGE_LLVM_PATH
|
||||
fi
|
||||
|
||||
# Install Flutter version for RustDesk library build
|
||||
|
||||
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
|
||||
|
||||
# gms is not in thoes files now, but we still keep the following line for future reference(maybe).
|
||||
# gms is not in these files now, but we still keep the following line for future reference(maybe).
|
||||
|
||||
sed \
|
||||
-i \
|
||||
@@ -414,13 +424,9 @@ build)
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
# Map NDK version to revision
|
||||
|
||||
NDK_VERSION="$(wget \
|
||||
-qO- \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
'https://api.github.com/repos/android/ndk/releases' |
|
||||
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||
NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
|
||||
jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
|
||||
sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
|
||||
|
||||
if [ -z "${NDK_VERSION}" ]; then
|
||||
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||
|
||||
@@ -1124,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,
|
||||
),
|
||||
@@ -1153,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,
|
||||
),
|
||||
);
|
||||
@@ -1578,7 +1579,7 @@ bool option2bool(String option, String value) {
|
||||
option == kOptionForceAlwaysRelay) {
|
||||
res = value == "Y";
|
||||
} else {
|
||||
assert(false);
|
||||
// "" is true
|
||||
res = value != "N";
|
||||
}
|
||||
return res;
|
||||
@@ -1596,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;
|
||||
@@ -2379,8 +2377,9 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
final password = uri.path.substring("/".length);
|
||||
if (password.isNotEmpty) {
|
||||
Timer(Duration(seconds: 1), () async {
|
||||
await bind.mainSetPermanentPassword(password: password);
|
||||
showToast(translate('Successful'));
|
||||
final ok =
|
||||
await bind.mainSetPermanentPasswordWithResult(password: password);
|
||||
showToast(translate(ok ? 'Successful' : 'Failed'));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2684,20 +2683,44 @@ class SimpleWrapper<T> {
|
||||
/// 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) {
|
||||
if (isLinux) return;
|
||||
_enabledKeys.add(key);
|
||||
WakelockPlus.enable();
|
||||
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 (isLinux) return;
|
||||
if (_enabledKeys.remove(key)) {
|
||||
if (_enabledKeys.isEmpty) {
|
||||
WakelockPlus.disable();
|
||||
if (isDesktop) {
|
||||
_enabledKeys.remove(key);
|
||||
if (_enabledKeys.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_enabled) {
|
||||
WakelockPlus.disable();
|
||||
_enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3041,6 +3064,11 @@ Future<void> start_service(bool is_start) async {
|
||||
}
|
||||
|
||||
Future<bool> canBeBlocked() async {
|
||||
if (isWeb) {
|
||||
// Web can only act as a controller, never as a controlled side,
|
||||
// so it should never be blocked by a remote session.
|
||||
return false;
|
||||
}
|
||||
// First check control permission
|
||||
final controlPermission = await bind.mainGetCommon(
|
||||
key: "is-remote-modify-enabled-by-control-permissions");
|
||||
@@ -4091,3 +4119,43 @@ String mouseButtonsToPeer(int buttons) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an avatar widget from an avatar URL or data URI string.
|
||||
/// Returns [fallback] if avatar is empty or cannot be decoded.
|
||||
/// [borderRadius] defaults to [size]/2 (circle).
|
||||
Widget? buildAvatarWidget({
|
||||
required String avatar,
|
||||
required double size,
|
||||
double? borderRadius,
|
||||
Widget? fallback,
|
||||
}) {
|
||||
final trimmed = avatar.trim();
|
||||
if (trimmed.isEmpty) return fallback;
|
||||
|
||||
ImageProvider? imageProvider;
|
||||
if (trimmed.startsWith('data:image/')) {
|
||||
final comma = trimmed.indexOf(',');
|
||||
if (comma > 0) {
|
||||
try {
|
||||
imageProvider = MemoryImage(base64Decode(trimmed.substring(comma + 1)));
|
||||
} catch (_) {}
|
||||
}
|
||||
} else if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
imageProvider = NetworkImage(trimmed);
|
||||
}
|
||||
|
||||
if (imageProvider == null) return fallback;
|
||||
|
||||
final radius = borderRadius ?? size / 2;
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: Image(
|
||||
image: imageProvider,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
fallback ?? SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ enum UserStatus { kDisabled, kNormal, kUnverified }
|
||||
// Is all the fields of the user needed?
|
||||
class UserPayload {
|
||||
String name = '';
|
||||
String displayName = '';
|
||||
String avatar = '';
|
||||
String email = '';
|
||||
String note = '';
|
||||
String? verifier;
|
||||
@@ -33,6 +35,8 @@ class UserPayload {
|
||||
|
||||
UserPayload.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'] ?? '',
|
||||
displayName = json['display_name'] ?? '',
|
||||
avatar = json['avatar'] ?? '',
|
||||
email = json['email'] ?? '',
|
||||
note = json['note'] ?? '',
|
||||
verifier = json['verifier'],
|
||||
@@ -46,6 +50,8 @@ class UserPayload {
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> map = {
|
||||
'name': name,
|
||||
'display_name': displayName,
|
||||
'avatar': avatar,
|
||||
'status': status == UserStatus.kDisabled
|
||||
? 0
|
||||
: status == UserStatus.kUnverified
|
||||
@@ -58,9 +64,14 @@ class UserPayload {
|
||||
Map<String, dynamic> toGroupCacheJson() {
|
||||
final Map<String, dynamic> map = {
|
||||
'name': name,
|
||||
'display_name': displayName,
|
||||
};
|
||||
return map;
|
||||
}
|
||||
|
||||
String get displayNameOrName {
|
||||
return displayName.trim().isEmpty ? name : displayName;
|
||||
}
|
||||
}
|
||||
|
||||
class PeerPayload {
|
||||
|
||||
@@ -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}"))),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -158,12 +158,18 @@ class _MyGroupState extends State<MyGroup> {
|
||||
return Obx(() {
|
||||
final userItems = gFFI.groupModel.users.where((p0) {
|
||||
if (searchAccessibleItemNameText.isNotEmpty) {
|
||||
return p0.name
|
||||
.toLowerCase()
|
||||
.contains(searchAccessibleItemNameText.value.toLowerCase());
|
||||
final search = searchAccessibleItemNameText.value.toLowerCase();
|
||||
return p0.name.toLowerCase().contains(search) ||
|
||||
p0.displayNameOrName.toLowerCase().contains(search);
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
// Count occurrences of each displayNameOrName to detect duplicates
|
||||
final displayNameCount = <String, int>{};
|
||||
for (final u in userItems) {
|
||||
final dn = u.displayNameOrName;
|
||||
displayNameCount[dn] = (displayNameCount[dn] ?? 0) + 1;
|
||||
}
|
||||
final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) {
|
||||
if (searchAccessibleItemNameText.isNotEmpty) {
|
||||
return p0.name
|
||||
@@ -177,7 +183,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
itemCount: deviceGroupItems.length + userItems.length,
|
||||
itemBuilder: (context, index) => index < deviceGroupItems.length
|
||||
? _buildDeviceGroupItem(deviceGroupItems[index])
|
||||
: _buildUserItem(userItems[index - deviceGroupItems.length]));
|
||||
: _buildUserItem(userItems[index - deviceGroupItems.length],
|
||||
displayNameCount));
|
||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||
? listView(false)
|
||||
@@ -185,8 +192,14 @@ class _MyGroupState extends State<MyGroup> {
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildUserItem(UserPayload user) {
|
||||
Widget _buildUserItem(UserPayload user, Map<String, int> displayNameCount) {
|
||||
final username = user.name;
|
||||
final dn = user.displayNameOrName;
|
||||
final isDuplicate = (displayNameCount[dn] ?? 0) > 1;
|
||||
final displayName =
|
||||
isDuplicate && user.displayName.trim().isNotEmpty
|
||||
? '${user.displayName} (@$username)'
|
||||
: dn;
|
||||
return InkWell(onTap: () {
|
||||
isSelectedDeviceGroup.value = false;
|
||||
if (selectedAccessibleItemName.value != username) {
|
||||
@@ -222,14 +235,14 @@ class _MyGroupState extends State<MyGroup> {
|
||||
alignment: Alignment.center,
|
||||
child: Center(
|
||||
child: Text(
|
||||
username.characters.first.toUpperCase(),
|
||||
displayName.characters.first.toUpperCase(),
|
||||
style: TextStyle(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
).marginOnly(right: 4),
|
||||
if (isMe) Flexible(child: Text(username)),
|
||||
if (isMe) Flexible(child: Text(displayName)),
|
||||
if (isMe)
|
||||
Flexible(
|
||||
child: Container(
|
||||
@@ -246,7 +259,7 @@ class _MyGroupState extends State<MyGroup> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isMe) Expanded(child: Text(username)),
|
||||
if (!isMe) Expanded(child: Text(displayName)),
|
||||
],
|
||||
).paddingSymmetric(vertical: 4),
|
||||
),
|
||||
|
||||
@@ -570,11 +570,14 @@ class MyGroupPeerView extends BasePeersView {
|
||||
static bool filter(Peer peer) {
|
||||
final model = gFFI.groupModel;
|
||||
if (model.searchAccessibleItemNameText.isNotEmpty) {
|
||||
final text = model.searchAccessibleItemNameText.value;
|
||||
final searchPeersOfUser = peer.loginName.contains(text) &&
|
||||
model.users.any((user) => user.name == peer.loginName);
|
||||
final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) &&
|
||||
model.deviceGroups.any((g) => g.name == peer.device_group_name);
|
||||
final text = model.searchAccessibleItemNameText.value.toLowerCase();
|
||||
final searchPeersOfUser = model.users.any((user) =>
|
||||
user.name == peer.loginName &&
|
||||
(user.name.toLowerCase().contains(text) ||
|
||||
user.displayNameOrName.toLowerCase().contains(text)));
|
||||
final searchPeersOfDeviceGroup =
|
||||
peer.device_group_name.toLowerCase().contains(text) &&
|
||||
model.deviceGroups.any((g) => g.name == peer.device_group_name);
|
||||
if (!searchPeersOfUser && !searchPeersOfDeviceGroup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -424,6 +437,14 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -557,6 +578,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
instance
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onOneFingerPanCancel = onOneFingerPanCancel
|
||||
..onTwoFingerScaleStart = onTwoFingerScaleStart
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
|
||||
@@ -175,6 +175,7 @@ const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
||||
const String kOptionHideStopService = "hide-stop-service";
|
||||
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
||||
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||
@@ -194,6 +195,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";
|
||||
|
||||
@@ -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, "", () {});
|
||||
@@ -904,12 +908,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
|
||||
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
final p0 = TextEditingController(text: pw);
|
||||
final p1 = TextEditingController(text: pw);
|
||||
final p0 = TextEditingController(text: "");
|
||||
final p1 = TextEditingController(text: "");
|
||||
var errMsg0 = "";
|
||||
var errMsg1 = "";
|
||||
final RxString rxPass = pw.trim().obs;
|
||||
final localPasswordSet =
|
||||
(await bind.mainGetCommon(key: "local-permanent-password-set")) == "true";
|
||||
final permanentPasswordSet =
|
||||
(await bind.mainGetCommon(key: "permanent-password-set")) == "true";
|
||||
final presetPassword = permanentPasswordSet && !localPasswordSet;
|
||||
var canSubmit = false;
|
||||
final RxString rxPass = "".obs;
|
||||
final rules = [
|
||||
DigitValidationRule(),
|
||||
UppercaseValidationRule(),
|
||||
@@ -918,9 +927,21 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
MinCharactersValidationRule(8),
|
||||
];
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
final statusTip = localPasswordSet
|
||||
? translate('password-hidden-tip')
|
||||
: (presetPassword ? translate('preset-password-in-use-tip') : '');
|
||||
final showStatusTipOnMobile =
|
||||
statusTip.isNotEmpty && !isDesktop && !isWebDesktop;
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
updateCanSubmit() {
|
||||
canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
submit() async {
|
||||
if (!canSubmit) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
errMsg1 = "";
|
||||
@@ -943,7 +964,13 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
});
|
||||
return;
|
||||
}
|
||||
bind.mainSetPermanentPassword(password: pass);
|
||||
final ok = await bind.mainSetPermanentPasswordWithResult(password: pass);
|
||||
if (!ok) {
|
||||
setState(() {
|
||||
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pass.isNotEmpty) {
|
||||
notEmptyCallback?.call();
|
||||
}
|
||||
@@ -951,14 +978,20 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set Password")),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.key, color: MyTheme.accent),
|
||||
Text(translate("Set Password")).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
SizedBox(
|
||||
height: showStatusTipOnMobile ? 0.0 : 6.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
@@ -974,6 +1007,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
rxPass.value = value.trim();
|
||||
setState(() {
|
||||
errMsg0 = '';
|
||||
updateCanSubmit();
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
@@ -985,9 +1019,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
children: [
|
||||
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
|
||||
],
|
||||
).marginSymmetric(vertical: 8),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8),
|
||||
SizedBox(
|
||||
height: showStatusTipOnMobile ? 0.0 : 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
@@ -1001,6 +1035,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
errMsg1 = '';
|
||||
updateCanSubmit();
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
@@ -1008,11 +1043,23 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
if (statusTip.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info, color: Colors.amber, size: 18)
|
||||
.marginOnly(right: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
statusTip,
|
||||
style: const TextStyle(fontSize: 13, height: 1.1),
|
||||
))
|
||||
],
|
||||
).marginOnly(top: 6, bottom: 2),
|
||||
SizedBox(
|
||||
height: showStatusTipOnMobile ? 0.0 : 8.0,
|
||||
),
|
||||
Obx(() => Wrap(
|
||||
runSpacing: 8,
|
||||
runSpacing: showStatusTipOnMobile ? 2.0 : 8.0,
|
||||
spacing: 4,
|
||||
children: rules.map((e) {
|
||||
var checked = e.validate(rxPass.value.trim());
|
||||
@@ -1032,11 +1079,67 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
actions: (() {
|
||||
final cancelButton = dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
);
|
||||
final removeButton = dialogButton(
|
||||
"Remove",
|
||||
icon: Icon(Icons.delete_outline_rounded),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
errMsg1 = "";
|
||||
});
|
||||
final ok =
|
||||
await bind.mainSetPermanentPasswordWithResult(password: "");
|
||||
if (!ok) {
|
||||
setState(() {
|
||||
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
|
||||
});
|
||||
return;
|
||||
}
|
||||
close();
|
||||
},
|
||||
buttonStyle: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||
);
|
||||
final okButton = dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: canSubmit ? submit : null,
|
||||
);
|
||||
if (!isDesktop && !isWebDesktop && localPasswordSet) {
|
||||
return [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
cancelButton,
|
||||
const SizedBox(width: 4),
|
||||
removeButton,
|
||||
const SizedBox(width: 4),
|
||||
okButton,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
return [
|
||||
cancelButton,
|
||||
if (localPasswordSet) removeButton,
|
||||
okButton,
|
||||
];
|
||||
})(),
|
||||
onSubmit: canSubmit ? submit : null,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -458,23 +458,31 @@ class _GeneralState extends State<_General> {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
return _Card(title: 'Service', children: [
|
||||
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
|
||||
() async {
|
||||
serviceBtnEnabled.value = false;
|
||||
await start_service(serviceStop.value);
|
||||
// enable the button after 1 second
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
serviceBtnEnabled.value = true;
|
||||
});
|
||||
}();
|
||||
}, enabled: serviceBtnEnabled.value))
|
||||
]);
|
||||
final hideStopService =
|
||||
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
|
||||
|
||||
return Obx(() {
|
||||
if (hideStopService && !serviceStop.value) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
return _Card(title: 'Service', children: [
|
||||
_Button(serviceStop.value ? 'Start' : 'Stop', () {
|
||||
() async {
|
||||
serviceBtnEnabled.value = false;
|
||||
await start_service(serviceStop.value);
|
||||
// enable the button after 1 second
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
serviceBtnEnabled.value = true;
|
||||
});
|
||||
}();
|
||||
}, enabled: serviceBtnEnabled.value)
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Widget other() {
|
||||
final showAutoUpdate =
|
||||
isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
|
||||
final showAutoUpdate = isWindows && bind.mainIsInstalled();
|
||||
final children = <Widget>[
|
||||
if (!isWeb && !bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
@@ -557,6 +565,17 @@ 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));
|
||||
@@ -1090,8 +1109,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
if (value ==
|
||||
passwordValues[passwordKeys
|
||||
.indexOf(kUsePermanentPassword)] &&
|
||||
(await bind.mainGetPermanentPassword())
|
||||
.isEmpty) {
|
||||
(await bind.mainGetCommon(
|
||||
key: "permanent-password-set")) !=
|
||||
"true") {
|
||||
if (isChangePermanentPasswordDisabled()) {
|
||||
await callback();
|
||||
return;
|
||||
@@ -1219,6 +1239,9 @@ 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',
|
||||
@@ -2002,7 +2025,9 @@ class _AccountState extends State<_Account> {
|
||||
|
||||
Widget accountAction() {
|
||||
return Obx(() => _Button(
|
||||
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
|
||||
gFFI.userModel.userName.value.isEmpty
|
||||
? 'Login'
|
||||
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})',
|
||||
() => {
|
||||
gFFI.userModel.userName.value.isEmpty
|
||||
? loginDialog()
|
||||
@@ -2011,24 +2036,65 @@ class _AccountState extends State<_Account> {
|
||||
}
|
||||
|
||||
Widget useInfo() {
|
||||
text(String key, String value) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SelectionArea(child: Text('${translate(key)}: $value'))
|
||||
.marginSymmetric(vertical: 4),
|
||||
);
|
||||
}
|
||||
|
||||
return Obx(() => Offstage(
|
||||
offstage: gFFI.userModel.userName.value.isEmpty,
|
||||
child: Column(
|
||||
children: [
|
||||
text('Username', gFFI.userModel.userName.value),
|
||||
// text('Group', gFFI.groupModel.groupName.value),
|
||||
],
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
final avatarWidget = _buildUserAvatar();
|
||||
return Row(
|
||||
children: [
|
||||
if (avatarWidget != null) avatarWidget,
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
gFFI.userModel.displayNameOrUserName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
SelectionArea(
|
||||
child: Text(
|
||||
'@${gFFI.userModel.userName.value}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color:
|
||||
Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
)).marginOnly(left: 18, top: 16);
|
||||
}
|
||||
|
||||
Widget? _buildUserAvatar() {
|
||||
// Resolve relative avatar path at display time
|
||||
final avatar =
|
||||
bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value);
|
||||
return buildAvatarWidget(
|
||||
avatar: avatar,
|
||||
size: 44,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Checkbox extends StatefulWidget {
|
||||
@@ -2116,7 +2182,9 @@ class _PluginState extends State<_Plugin> {
|
||||
|
||||
Widget accountAction() {
|
||||
return Obx(() => _Button(
|
||||
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
|
||||
gFFI.userModel.userName.value.isEmpty
|
||||
? 'Login'
|
||||
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})',
|
||||
() => {
|
||||
gFFI.userModel.userName.value.isEmpty
|
||||
? loginDialog()
|
||||
@@ -2524,6 +2592,49 @@ class WaylandCard extends StatefulWidget {
|
||||
|
||||
class _WaylandCardState extends State<WaylandCard> {
|
||||
final restoreTokenKey = 'wayland-restore-token';
|
||||
static const _kClearShortcutsInhibitorEventKey =
|
||||
'clear-gnome-shortcuts-inhibitor-permission-res';
|
||||
final _clearShortcutsInhibitorFailedMsg = ''.obs;
|
||||
// Don't show the shortcuts permission reset button for now.
|
||||
// Users can change it manually:
|
||||
// "Settings" -> "Apps" -> "RustDesk" -> "Permissions" -> "Inhibit Shortcuts".
|
||||
// For resetting(clearing) the permission from the portal permission store, you can
|
||||
// use (replace <desktop-id> with the RustDesk desktop file ID):
|
||||
// busctl --user call org.freedesktop.impl.portal.PermissionStore \
|
||||
// /org/freedesktop/impl/portal/PermissionStore org.freedesktop.impl.portal.PermissionStore \
|
||||
// DeletePermission sss "gnome" "shortcuts-inhibitor" "<desktop-id>"
|
||||
// On a native install this is typically "rustdesk.desktop"; on Flatpak it is usually
|
||||
// the exported desktop ID derived from the Flatpak app-id (e.g. "com.rustdesk.RustDesk.desktop").
|
||||
//
|
||||
// We may add it back in the future if needed.
|
||||
final showResetInhibitorPermission = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (showResetInhibitorPermission) {
|
||||
platformFFI.registerEventHandler(
|
||||
_kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey,
|
||||
(evt) async {
|
||||
if (!mounted) return;
|
||||
if (evt['success'] == true) {
|
||||
setState(() {});
|
||||
} else {
|
||||
_clearShortcutsInhibitorFailedMsg.value =
|
||||
evt['msg'] as String? ?? 'Unknown error';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (showResetInhibitorPermission) {
|
||||
platformFFI.unregisterEventHandler(
|
||||
_kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -2531,9 +2642,16 @@ class _WaylandCardState extends State<WaylandCard> {
|
||||
future: bind.mainHandleWaylandScreencastRestoreToken(
|
||||
key: restoreTokenKey, value: "get"),
|
||||
hasData: (restoreToken) {
|
||||
final hasShortcutsPermission = showResetInhibitorPermission &&
|
||||
bind.mainGetCommonSync(
|
||||
key: "has-gnome-shortcuts-inhibitor-permission") ==
|
||||
"true";
|
||||
|
||||
final children = [
|
||||
if (restoreToken.isNotEmpty)
|
||||
_buildClearScreenSelection(context, restoreToken),
|
||||
if (hasShortcutsPermission)
|
||||
_buildClearShortcutsInhibitorPermission(context),
|
||||
];
|
||||
return Offstage(
|
||||
offstage: children.isEmpty,
|
||||
@@ -2578,6 +2696,50 @@ class _WaylandCardState extends State<WaylandCard> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClearShortcutsInhibitorPermission(BuildContext context) {
|
||||
onConfirm() {
|
||||
_clearShortcutsInhibitorFailedMsg.value = '';
|
||||
bind.mainSetCommon(
|
||||
key: "clear-gnome-shortcuts-inhibitor-permission", value: "");
|
||||
gFFI.dialogManager.dismissAll();
|
||||
}
|
||||
|
||||
showConfirmMsgBox() => msgBoxCommon(
|
||||
gFFI.dialogManager,
|
||||
'Confirmation',
|
||||
Text(
|
||||
translate('confirm-clear-shortcuts-inhibitor-permission-tip'),
|
||||
),
|
||||
[
|
||||
dialogButton('OK', onPressed: onConfirm),
|
||||
dialogButton('Cancel',
|
||||
onPressed: () => gFFI.dialogManager.dismissAll())
|
||||
]);
|
||||
|
||||
return Column(children: [
|
||||
Obx(
|
||||
() => _clearShortcutsInhibitorFailedMsg.value.isEmpty
|
||||
? Offstage()
|
||||
: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(_clearShortcutsInhibitorFailedMsg.value,
|
||||
style: DefaultTextStyle.of(context)
|
||||
.style
|
||||
.copyWith(color: Colors.red))
|
||||
.marginOnly(bottom: 10.0)),
|
||||
),
|
||||
_Button(
|
||||
'Reset keyboard shortcuts permission',
|
||||
showConfirmMsgBox,
|
||||
tip: 'clear-shortcuts-inhibitor-permission-tip',
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.error.withOpacity(0.75)),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
|
||||
@@ -462,23 +462,7 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: str2color(client.name),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Text(
|
||||
client.name[0],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 55,
|
||||
),
|
||||
),
|
||||
).marginOnly(right: 10.0),
|
||||
_buildClientAvatar().marginOnly(right: 10.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -582,6 +566,36 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
Widget _buildClientAvatar() {
|
||||
return buildAvatarWidget(
|
||||
avatar: client.avatar,
|
||||
size: 70,
|
||||
borderRadius: 15,
|
||||
fallback: _buildInitialAvatar(),
|
||||
) ??
|
||||
_buildInitialAvatar();
|
||||
}
|
||||
|
||||
Widget _buildInitialAvatar() {
|
||||
return Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: str2color(client.name),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Text(
|
||||
client.name.isNotEmpty ? client.name[0] : '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 55,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrivilegeBoard extends StatefulWidget {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -15,6 +16,7 @@ class TerminalPage extends StatefulWidget {
|
||||
required this.tabController,
|
||||
required this.isSharedPassword,
|
||||
required this.terminalId,
|
||||
required this.tabKey,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
@@ -25,6 +27,8 @@ class TerminalPage extends StatefulWidget {
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final int terminalId;
|
||||
/// Tab key for focus management, passed from parent to avoid duplicate construction
|
||||
final String tabKey;
|
||||
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
|
||||
@@ -42,11 +46,16 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
double? _cellHeight;
|
||||
final FocusNode _terminalFocusNode = FocusNode(canRequestFocus: false);
|
||||
StreamSubscription<DesktopTabState>? _tabStateSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Listen for tab selection changes to request focus
|
||||
_tabStateSubscription = widget.tabController.state.listen(_onTabStateChanged);
|
||||
|
||||
// Use shared FFI instance from connection manager
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
peerId: widget.id,
|
||||
@@ -64,6 +73,13 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
||||
_cellHeight = ph * 1.0;
|
||||
|
||||
// Enable focus once terminal has valid dimensions (first valid resize)
|
||||
if (!_terminalFocusNode.canRequestFocus && w > 0 && h > 0) {
|
||||
_terminalFocusNode.canRequestFocus = true;
|
||||
// Auto-focus if this tab is currently selected
|
||||
_requestFocusIfSelected();
|
||||
}
|
||||
|
||||
// Schedule the setState for the next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
@@ -99,14 +115,42 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Cancel tab state subscription to prevent memory leak
|
||||
_tabStateSubscription?.cancel();
|
||||
// Unregister terminal model from FFI
|
||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||
_terminalModel.dispose();
|
||||
_terminalFocusNode.dispose();
|
||||
// Release connection reference instead of closing directly
|
||||
TerminalConnectionManager.releaseConnection(widget.id);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabStateChanged(DesktopTabState state) {
|
||||
// Check if this tab is now selected and request focus
|
||||
if (state.selected >= 0 && state.selected < state.tabs.length) {
|
||||
final selectedTab = state.tabs[state.selected];
|
||||
if (selectedTab.key == widget.tabKey && mounted) {
|
||||
_requestFocusIfSelected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _requestFocusIfSelected() {
|
||||
if (!mounted || !_terminalFocusNode.canRequestFocus) return;
|
||||
// Use post-frame callback to ensure widget is fully laid out in focus tree
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Re-check conditions after frame: mounted, focusable, still selected, not already focused
|
||||
if (!mounted || !_terminalFocusNode.canRequestFocus || _terminalFocusNode.hasFocus) return;
|
||||
final state = widget.tabController.state.value;
|
||||
if (state.selected >= 0 && state.selected < state.tabs.length) {
|
||||
if (state.tabs[state.selected].key == widget.tabKey) {
|
||||
_terminalFocusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This method ensures that the number of visible rows is an integer by computing the
|
||||
// extra space left after dividing the available height by the height of a single
|
||||
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||
@@ -131,7 +175,9 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
return TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
focusNode: _terminalFocusNode,
|
||||
// Note: autofocus is not used here because focus is managed manually
|
||||
// via _onTabStateChanged() to handle tab switching properly.
|
||||
backgroundOpacity: 0.7,
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
|
||||
@@ -34,6 +34,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
static const IconData selectedIcon = Icons.terminal;
|
||||
static const IconData unselectedIcon = Icons.terminal_outlined;
|
||||
int _nextTerminalId = 1;
|
||||
// Lightweight idempotency guard for async close operations
|
||||
final Set<String> _closingTabs = {};
|
||||
// When true, all session cleanup should persist (window-level close in progress)
|
||||
bool _windowClosing = false;
|
||||
|
||||
_TerminalTabPageState(Map<String, dynamic> params) {
|
||||
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
|
||||
@@ -70,28 +74,12 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
label: tabLabel,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabKey,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
// Close the terminal session first
|
||||
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
||||
if (ffi != null) {
|
||||
final terminalModel = ffi.terminalModels[terminalId];
|
||||
if (terminalModel != null) {
|
||||
await terminalModel.closeTerminal();
|
||||
}
|
||||
}
|
||||
// Then close the tab
|
||||
tabController.closeBy(tabKey);
|
||||
},
|
||||
onTabCloseButton: () => _closeTab(tabKey),
|
||||
page: TerminalPage(
|
||||
key: ValueKey(tabKey),
|
||||
id: peerId,
|
||||
terminalId: terminalId,
|
||||
tabKey: tabKey,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
tabController: tabController,
|
||||
@@ -101,6 +89,159 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Unified tab close handler for all close paths (button, shortcut, programmatic).
|
||||
/// Shows audit dialog, cleans up session if not persistent, then removes the UI tab.
|
||||
Future<void> _closeTab(String tabKey) async {
|
||||
// Idempotency guard: skip if already closing this tab
|
||||
if (_closingTabs.contains(tabKey)) return;
|
||||
_closingTabs.add(tabKey);
|
||||
|
||||
try {
|
||||
// Snapshot peerTabCount BEFORE any await to avoid race with concurrent
|
||||
// _closeAllTabs clearing tabController (which would make the live count
|
||||
// drop to 0 and incorrectly trigger session persistence).
|
||||
// Note: the snapshot may become stale if other individual tabs are closed
|
||||
// during the audit dialog, but this is an acceptable trade-off.
|
||||
int? snapshotPeerTabCount;
|
||||
final parsed = _parseTabKey(tabKey);
|
||||
if (parsed != null) {
|
||||
final (peerId, _) = parsed;
|
||||
snapshotPeerTabCount = tabController.state.value.tabs.where((t) {
|
||||
final p = _parseTabKey(t.key);
|
||||
return p != null && p.$1 == peerId;
|
||||
}).length;
|
||||
}
|
||||
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabKey,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close terminal session if not in persistent mode.
|
||||
// Wrapped separately so session cleanup failure never blocks UI tab removal.
|
||||
try {
|
||||
await _closeTerminalSessionIfNeeded(tabKey,
|
||||
peerTabCount: snapshotPeerTabCount);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e');
|
||||
}
|
||||
// Always close the tab from UI, regardless of session cleanup result
|
||||
tabController.closeBy(tabKey);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalTabPage] Error closing tab $tabKey: $e');
|
||||
} finally {
|
||||
_closingTabs.remove(tabKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// Close all tabs with session cleanup.
|
||||
/// Used for window-level close operations (onDestroy, handleWindowCloseButton).
|
||||
/// UI tabs are removed immediately; session cleanup runs in parallel with a
|
||||
/// bounded timeout so window close is not blocked indefinitely.
|
||||
Future<void> _closeAllTabs() async {
|
||||
_windowClosing = true;
|
||||
final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList();
|
||||
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
|
||||
tabController.clear();
|
||||
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
|
||||
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
|
||||
final futures = tabKeys
|
||||
.where((tabKey) => !_closingTabs.contains(tabKey))
|
||||
.map((tabKey) async {
|
||||
try {
|
||||
await _closeTerminalSessionIfNeeded(tabKey, persistAll: true);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e');
|
||||
}
|
||||
}).toList();
|
||||
if (futures.isNotEmpty) {
|
||||
await Future.wait(futures).timeout(
|
||||
const Duration(seconds: 4),
|
||||
onTimeout: () {
|
||||
debugPrint(
|
||||
'[TerminalTabPage] Session cleanup timed out for batch close');
|
||||
return [];
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the terminal session on server side based on persistent mode.
|
||||
///
|
||||
/// [persistAll] controls behavior when persistent mode is enabled:
|
||||
/// - `true` (window close): persist all sessions, don't close any.
|
||||
/// - `false` (tab close): only persist the last session for the peer,
|
||||
/// close others so only the most recent disconnected session survives.
|
||||
///
|
||||
/// Note: if [_windowClosing] is true, persistAll is forced to true so that
|
||||
/// in-flight _closeTab() calls don't accidentally close sessions that the
|
||||
/// window-close flow intends to preserve.
|
||||
Future<void> _closeTerminalSessionIfNeeded(String tabKey,
|
||||
{bool persistAll = false, int? peerTabCount}) async {
|
||||
// If window close is in progress, override to persist all sessions
|
||||
// even if this call originated from an individual tab close.
|
||||
if (_windowClosing) {
|
||||
persistAll = true;
|
||||
}
|
||||
final parsed = _parseTabKey(tabKey);
|
||||
if (parsed == null) return;
|
||||
final (peerId, terminalId) = parsed;
|
||||
|
||||
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
||||
if (ffi == null) return;
|
||||
|
||||
final isPersistent = bind.sessionGetToggleOptionSync(
|
||||
sessionId: ffi.sessionId,
|
||||
arg: kOptionTerminalPersistent,
|
||||
);
|
||||
|
||||
if (isPersistent) {
|
||||
if (persistAll) {
|
||||
// Window close: persist all sessions
|
||||
return;
|
||||
}
|
||||
// Tab close: only persist if this is the last tab for this peer.
|
||||
// Use the snapshot value if provided (avoids race with concurrent tab removal).
|
||||
final effectivePeerTabCount = peerTabCount ??
|
||||
tabController.state.value.tabs.where((t) {
|
||||
final p = _parseTabKey(t.key);
|
||||
return p != null && p.$1 == peerId;
|
||||
}).length;
|
||||
if (effectivePeerTabCount <= 1) {
|
||||
// Last tab for this peer — persist the session
|
||||
return;
|
||||
}
|
||||
// Not the last tab — fall through to close the session
|
||||
}
|
||||
|
||||
final terminalModel = ffi.terminalModels[terminalId];
|
||||
if (terminalModel != null) {
|
||||
// closeTerminal() has internal 3s timeout, no need for external timeout
|
||||
await terminalModel.closeTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse tabKey (format: "peerId_terminalId") into its components.
|
||||
/// Note: peerId may contain underscores, so we use lastIndexOf('_').
|
||||
/// Returns null if tabKey format is invalid.
|
||||
(String peerId, int terminalId)? _parseTabKey(String tabKey) {
|
||||
final lastUnderscore = tabKey.lastIndexOf('_');
|
||||
if (lastUnderscore <= 0) {
|
||||
debugPrint('[TerminalTabPage] Invalid tabKey format: $tabKey');
|
||||
return null;
|
||||
}
|
||||
final terminalIdStr = tabKey.substring(lastUnderscore + 1);
|
||||
final terminalId = int.tryParse(terminalIdStr);
|
||||
if (terminalId == null) {
|
||||
debugPrint('[TerminalTabPage] Invalid terminalId in tabKey: $tabKey');
|
||||
return null;
|
||||
}
|
||||
final peerId = tabKey.substring(0, lastUnderscore);
|
||||
return (peerId, terminalId);
|
||||
}
|
||||
|
||||
Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) {
|
||||
final List<MenuEntryBase<String>> menu = [];
|
||||
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
|
||||
@@ -184,7 +325,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
} else if (call.method == kWindowEventRestoreTerminalSessions) {
|
||||
_restoreSessions(call.arguments);
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
// Clean up sessions before window destruction (bounded wait)
|
||||
await _closeAllTabs();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventActiveSession) {
|
||||
@@ -194,7 +336,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
assert(call.arguments is String,
|
||||
"Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}");
|
||||
if (currentTab.key.startsWith(call.arguments)) {
|
||||
// Use lastIndexOf to handle peerIds containing underscores
|
||||
final lastUnderscore = currentTab.key.lastIndexOf('_');
|
||||
if (lastUnderscore > 0 &&
|
||||
currentTab.key.substring(0, lastUnderscore) == call.arguments) {
|
||||
windowOnTop(windowId());
|
||||
return true;
|
||||
}
|
||||
@@ -265,7 +410,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
// macOS: Cmd+W (standard for close tab)
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
if (tabController.state.value.tabs.length > 1) {
|
||||
tabController.closeBy(currentTab.key);
|
||||
_closeTab(currentTab.key);
|
||||
return true;
|
||||
}
|
||||
} else if (!isMacOS &&
|
||||
@@ -274,7 +419,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
// Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete)
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
if (tabController.state.value.tabs.length > 1) {
|
||||
tabController.closeBy(currentTab.key);
|
||||
_closeTab(currentTab.key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -329,7 +474,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
void _addNewTerminal(String peerId, {int? terminalId}) {
|
||||
// Find first tab for this peer to get connection parameters
|
||||
final firstTab = tabController.state.value.tabs.firstWhere(
|
||||
(tab) => tab.key.startsWith('$peerId\_'),
|
||||
(tab) {
|
||||
final last = tab.key.lastIndexOf('_');
|
||||
return last > 0 && tab.key.substring(0, last) == peerId;
|
||||
},
|
||||
);
|
||||
if (firstTab.page is TerminalPage) {
|
||||
final page = firstTab.page as TerminalPage;
|
||||
@@ -350,11 +498,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
|
||||
void _addNewTerminalForCurrentPeer({int? terminalId}) {
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
final parts = currentTab.key.split('_');
|
||||
if (parts.isNotEmpty) {
|
||||
final peerId = parts[0];
|
||||
_addNewTerminal(peerId, terminalId: terminalId);
|
||||
}
|
||||
final parsed = _parseTabKey(currentTab.key);
|
||||
if (parsed == null) return;
|
||||
final (peerId, _) = parsed;
|
||||
_addNewTerminal(peerId, terminalId: terminalId);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -368,10 +515,9 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabMenuBuilder: (key) {
|
||||
// Extract peerId from tab key (format: "peerId_terminalId")
|
||||
final parts = key.split('_');
|
||||
if (parts.isEmpty) return Container();
|
||||
final peerId = parts[0];
|
||||
final parsed = _parseTabKey(key);
|
||||
if (parsed == null) return Container();
|
||||
final (peerId, _) = parsed;
|
||||
return _tabMenuBuilder(peerId, () {});
|
||||
},
|
||||
));
|
||||
@@ -426,7 +572,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
await _closeAllTabs();
|
||||
return true;
|
||||
} else {
|
||||
final bool res;
|
||||
@@ -437,7 +583,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
res = await closeConfirmDialog();
|
||||
}
|
||||
if (res) {
|
||||
tabController.clear();
|
||||
await _closeAllTabs();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1861,8 +1861,18 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pi.isWayland && mode.key != kKeyMapMode) {
|
||||
continue;
|
||||
if (pi.isWayland) {
|
||||
// Legacy mode is hidden on desktop control side because dead keys
|
||||
// don't work properly on Wayland. When the control side is mobile,
|
||||
// Legacy mode is used automatically (mobile always sends Legacy events).
|
||||
if (mode.key == kKeyLegacyMode) {
|
||||
continue;
|
||||
}
|
||||
// Translate mode requires server >= 1.4.6.
|
||||
if (mode.key == kKeyTranslateMode &&
|
||||
versionCmp(pi.version, '1.4.6') < 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var text = translate(mode.menu);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -14,7 +13,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';
|
||||
@@ -66,9 +64,8 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
bool _showGestureHelp = false;
|
||||
String _value = '';
|
||||
Orientation? _currentOrientation;
|
||||
double _viewInsetsBottom = 0;
|
||||
|
||||
Timer? _timerDidChangeMetrics;
|
||||
final _uniqueKey = UniqueKey();
|
||||
Timer? _iosKeyboardWorkaroundTimer;
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
@@ -105,9 +102,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);
|
||||
@@ -142,13 +137,11 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
_physicalFocusNode.dispose();
|
||||
await gFFI.close();
|
||||
_timer?.cancel();
|
||||
_timerDidChangeMetrics?.cancel();
|
||||
_iosKeyboardWorkaroundTimer?.cancel();
|
||||
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.
|
||||
@@ -170,26 +163,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
gFFI.invokeMethod("try_sync_clipboard");
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
|
||||
// Don't try reset the view style and focus the cursor.
|
||||
if (gFFI.cursorModel.lastKeyboardIsVisible &&
|
||||
gFFI.canvasModel.isMobileCanvasChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
|
||||
_timerDidChangeMetrics?.cancel();
|
||||
_timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
|
||||
// We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
|
||||
if (newBottom != _viewInsetsBottom) {
|
||||
gFFI.canvasModel.mobileFocusCanvasCursor();
|
||||
_viewInsetsBottom = newBottom;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||
// But for now, the transparent color will cause the canvas to be white.
|
||||
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
||||
@@ -211,7 +184,24 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
gFFI.ffiModel.pi.version.isNotEmpty) {
|
||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||
}
|
||||
|
||||
// Workaround for iOS: physical keyboard input fails after virtual keyboard is hidden
|
||||
// https://github.com/flutter/flutter/issues/39900
|
||||
// https://github.com/rustdesk/rustdesk/discussions/11843#discussioncomment-13499698 - Virtual keyboard issue
|
||||
if (isIOS) {
|
||||
_iosKeyboardWorkaroundTimer?.cancel();
|
||||
_iosKeyboardWorkaroundTimer = Timer(Duration(milliseconds: 100), () {
|
||||
if (!mounted) return;
|
||||
_physicalFocusNode.unfocus();
|
||||
_iosKeyboardWorkaroundTimer = Timer(Duration(milliseconds: 50), () {
|
||||
if (!mounted) return;
|
||||
_physicalFocusNode.requestFocus();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_iosKeyboardWorkaroundTimer?.cancel();
|
||||
_iosKeyboardWorkaroundTimer = null;
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
|
||||
@@ -150,7 +150,8 @@ class _DropDownAction extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (value == kUsePermanentPassword &&
|
||||
(await bind.mainGetPermanentPassword()).isEmpty) {
|
||||
(await bind.mainGetCommon(key: "permanent-password-set")) !=
|
||||
"true") {
|
||||
if (isChangePermanentPasswordDisabled()) {
|
||||
callback();
|
||||
return;
|
||||
@@ -582,10 +583,13 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
Widget build(BuildContext context) {
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
final hasAudioPermission = androidVersion >= 30;
|
||||
final hideStopService =
|
||||
isAndroid &&
|
||||
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
|
||||
return PaddingCard(
|
||||
title: translate("Permissions"),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
serverModel.mediaOk
|
||||
serverModel.mediaOk && !hideStopService
|
||||
? ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
@@ -595,14 +599,15 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
label: Text(translate("Stop service")))
|
||||
.marginOnly(bottom: 8)
|
||||
: SizedBox.shrink(),
|
||||
PermissionRow(
|
||||
translate("Screen Capture"),
|
||||
serverModel.mediaOk,
|
||||
!serverModel.mediaOk &&
|
||||
gFFI.userModel.userName.value.isEmpty &&
|
||||
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
||||
? () => showScamWarning(context, serverModel)
|
||||
: serverModel.toggleService),
|
||||
if (!hideStopService || !serverModel.mediaOk)
|
||||
PermissionRow(
|
||||
translate("Screen Capture"),
|
||||
serverModel.mediaOk,
|
||||
!serverModel.mediaOk &&
|
||||
gFFI.userModel.userName.value.isEmpty &&
|
||||
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
||||
? () => showScamWarning(context, serverModel)
|
||||
: serverModel.toggleService),
|
||||
PermissionRow(translate("Input Control"), serverModel.inputOk,
|
||||
serverModel.toggleInput),
|
||||
PermissionRow(translate("Transfer file"), serverModel.fileOk,
|
||||
@@ -841,13 +846,7 @@ class ClientInfo extends StatelessWidget {
|
||||
flex: -1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: str2color(
|
||||
client.name,
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? 255
|
||||
: 150),
|
||||
child: Text(client.name[0])))),
|
||||
child: _buildAvatar(context))),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -860,6 +859,20 @@ class ClientInfo extends StatelessWidget {
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
Widget _buildAvatar(BuildContext context) {
|
||||
final fallback = CircleAvatar(
|
||||
backgroundColor: str2color(client.name,
|
||||
Theme.of(context).brightness == Brightness.light ? 255 : 150),
|
||||
child: Text(client.name.isNotEmpty ? client.name[0] : '?'),
|
||||
);
|
||||
return buildAvatarWidget(
|
||||
avatar: client.avatar,
|
||||
size: 40,
|
||||
fallback: fallback,
|
||||
) ??
|
||||
fallback;
|
||||
}
|
||||
}
|
||||
|
||||
void androidChannelInit() {
|
||||
|
||||
@@ -100,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(
|
||||
@@ -140,6 +141,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
||||
_allowAskForNoteAtEndOfConnection =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
_preventSleepWhileConnected =
|
||||
mainGetLocalBoolOptionSync(kOptionKeepAwakeDuringOutgoingSessions);
|
||||
_showTerminalExtraKeys =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
}
|
||||
@@ -614,7 +617,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
onToggle: (bool v) async {
|
||||
await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v);
|
||||
final newValue =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
setState(() {
|
||||
_showTerminalExtraKeys = newValue;
|
||||
});
|
||||
@@ -685,8 +688,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
SettingsTile(
|
||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate('Login')
|
||||
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
||||
leading: Icon(Icons.person),
|
||||
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
|
||||
leading: Obx(() {
|
||||
final avatar = bind.mainResolveAvatarUrl(
|
||||
avatar: gFFI.userModel.avatar.value);
|
||||
return buildAvatarWidget(
|
||||
avatar: avatar,
|
||||
size: 28,
|
||||
borderRadius: null,
|
||||
fallback: Icon(Icons.person),
|
||||
) ??
|
||||
Icon(Icons.person);
|
||||
}),
|
||||
onPressed: (context) {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
@@ -823,7 +836,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_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,5 +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';
|
||||
@@ -41,6 +42,9 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
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.
|
||||
@@ -79,7 +83,10 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
_showTerminalExtraKeys = mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
// Web desktop users have full hardware keyboard access, so the on-screen
|
||||
// terminal extra keys bar is unnecessary and disabled.
|
||||
_showTerminalExtraKeys = !isWebDesktop &&
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
@@ -147,7 +154,7 @@ 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: Stack(
|
||||
@@ -164,6 +171,13 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
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;
|
||||
@@ -185,9 +199,108 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
),
|
||||
),
|
||||
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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -12,100 +12,6 @@ void _showSuccess() {
|
||||
showToast(translate("Successful"));
|
||||
}
|
||||
|
||||
void _showError() {
|
||||
showToast(translate("Error"));
|
||||
}
|
||||
|
||||
void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
final p0 = TextEditingController(text: pw);
|
||||
final p1 = TextEditingController(text: pw);
|
||||
var validateLength = false;
|
||||
var validateSame = false;
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
close();
|
||||
dialogManager.showLoading(translate("Waiting"));
|
||||
if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
|
||||
dialogManager.dismissAll();
|
||||
_showSuccess();
|
||||
} else {
|
||||
dialogManager.dismissAll();
|
||||
_showError();
|
||||
}
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('Set your own password')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Password'),
|
||||
),
|
||||
controller: p0,
|
||||
validator: (v) {
|
||||
if (v == null) return null;
|
||||
final val = v.trim().length > 5;
|
||||
if (validateLength != val) {
|
||||
// use delay to make setState success
|
||||
Future.delayed(Duration(microseconds: 1),
|
||||
() => setState(() => validateLength = val));
|
||||
}
|
||||
return val
|
||||
? null
|
||||
: translate('Too short, at least 6 characters.');
|
||||
},
|
||||
).workaroundFreezeLinuxMint(),
|
||||
TextFormField(
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Confirmation'),
|
||||
),
|
||||
controller: p1,
|
||||
validator: (v) {
|
||||
if (v == null) return null;
|
||||
final val = p0.text == v;
|
||||
if (validateSame != val) {
|
||||
Future.delayed(Duration(microseconds: 1),
|
||||
() => setState(() => validateSame = val));
|
||||
}
|
||||
return val
|
||||
? null
|
||||
: translate('The confirmation is not identical.');
|
||||
},
|
||||
).workaroundFreezeLinuxMint(),
|
||||
])),
|
||||
onCancel: close,
|
||||
onSubmit: (validateLength && validateSame) ? submit : null,
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'OK',
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: (validateLength && validateSame) ? submit : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void setTemporaryPasswordLengthDialog(
|
||||
OverlayDialogManager dialogManager) async {
|
||||
List<String> lengths = ['6', '8', '10'];
|
||||
|
||||
@@ -59,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;
|
||||
}
|
||||
@@ -347,6 +348,12 @@ class InputModel {
|
||||
final _trackpadAdjustPeerLinux = 0.06;
|
||||
// This is an experience value.
|
||||
final _trackpadAdjustMacToWin = 2.50;
|
||||
// Ignore directional locking for very small deltas on both axes (including
|
||||
// tiny single-axis movement) to avoid over-filtering near zero.
|
||||
static const double _trackpadAxisNoiseThreshold = 0.2;
|
||||
// Lock to dominant axis only when one axis is clearly stronger.
|
||||
// 1.6 means the dominant axis must be >= 60% larger than the other.
|
||||
static const double _trackpadAxisLockRatio = 1.6;
|
||||
int _trackpadSpeed = kDefaultTrackpadSpeed;
|
||||
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
|
||||
var _trackpadScrollUnsent = Offset.zero;
|
||||
@@ -364,6 +371,16 @@ class InputModel {
|
||||
final isPhysicalMouse = false.obs;
|
||||
int _lastButtons = 0;
|
||||
Offset lastMousePos = Offset.zero;
|
||||
int _lastWheelTsUs = 0;
|
||||
|
||||
// Wheel acceleration thresholds.
|
||||
static const int _wheelAccelFastThresholdUs = 40000; // 40ms
|
||||
static const int _wheelAccelMediumThresholdUs = 80000; // 80ms
|
||||
static const double _wheelBurstVelocityThreshold =
|
||||
0.002; // delta units per microsecond
|
||||
// Wheel burst acceleration (empirical tuning).
|
||||
// Applies only to fast, non-smooth bursts to preserve single-step scrolling.
|
||||
// Flutter uses microseconds for dt, so velocity is in delta/us.
|
||||
|
||||
// Relative mouse mode (for games/3D apps).
|
||||
final relativeMouseMode = false.obs;
|
||||
@@ -418,6 +435,74 @@ class InputModel {
|
||||
});
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Because `sessionGetKeyboardMode` relies on the peer version.
|
||||
updateKeyboardMode() async {
|
||||
@@ -550,6 +635,11 @@ class InputModel {
|
||||
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) {
|
||||
@@ -586,7 +676,7 @@ class InputModel {
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
mapKeyboardModeRaw(e);
|
||||
mapKeyboardModeRaw(e, iosCapsLock);
|
||||
} else {
|
||||
legacyKeyboardModeRaw(e);
|
||||
}
|
||||
@@ -622,6 +712,11 @@ class InputModel {
|
||||
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) {
|
||||
@@ -667,7 +762,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);
|
||||
}
|
||||
@@ -676,23 +772,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,
|
||||
@@ -701,7 +783,7 @@ class InputModel {
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
void mapKeyboardModeRaw(RawKeyEvent e) {
|
||||
void mapKeyboardModeRaw(RawKeyEvent e, bool iosCapsLock) {
|
||||
int positionCode = -1;
|
||||
int platformCode = -1;
|
||||
bool down;
|
||||
@@ -732,27 +814,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,
|
||||
@@ -826,6 +895,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) {
|
||||
@@ -850,7 +922,7 @@ class InputModel {
|
||||
buttons = evt.buttons;
|
||||
}
|
||||
}
|
||||
_lastButtons = evt.buttons;
|
||||
_lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons;
|
||||
|
||||
out['buttons'] = buttons;
|
||||
out['type'] = type;
|
||||
@@ -908,6 +980,7 @@ class InputModel {
|
||||
toReleaseRawKeys.release(handleRawKeyEvent);
|
||||
_pointerMovedAfterEnter = false;
|
||||
_pointerInsideImage = enter;
|
||||
_lastWheelTsUs = 0;
|
||||
|
||||
// Fix status
|
||||
if (!enter) {
|
||||
@@ -1048,6 +1121,14 @@ class InputModel {
|
||||
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) {
|
||||
@@ -1097,6 +1178,7 @@ class InputModel {
|
||||
if (isMacOS && peerPlatform == kPeerPlatformWindows) {
|
||||
delta *= _trackpadAdjustMacToWin;
|
||||
}
|
||||
delta = _filterTrackpadDeltaAxis(delta);
|
||||
_trackpadLastDelta = delta;
|
||||
|
||||
var x = delta.dx.toInt();
|
||||
@@ -1129,6 +1211,24 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
Offset _filterTrackpadDeltaAxis(Offset delta) {
|
||||
final absDx = delta.dx.abs();
|
||||
final absDy = delta.dy.abs();
|
||||
// Keep diagonal intent when movement is tiny on both axes.
|
||||
if (absDx < _trackpadAxisNoiseThreshold &&
|
||||
absDy < _trackpadAxisNoiseThreshold) {
|
||||
return delta;
|
||||
}
|
||||
// Dominant-axis lock to reduce accidental cross-axis scrolling noise.
|
||||
if (absDy >= absDx * _trackpadAxisLockRatio) {
|
||||
return Offset(0, delta.dy);
|
||||
}
|
||||
if (absDx >= absDy * _trackpadAxisLockRatio) {
|
||||
return Offset(delta.dx, 0);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
void _scheduleFling(double x, double y, int delay) {
|
||||
if (isViewCamera) return;
|
||||
if ((x == 0 && y == 0) || _stopFling) {
|
||||
@@ -1210,6 +1310,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;
|
||||
@@ -1219,6 +1341,13 @@ class InputModel {
|
||||
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);
|
||||
}
|
||||
@@ -1314,17 +1443,44 @@ class InputModel {
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
if (e is PointerScrollEvent) {
|
||||
var dx = e.scrollDelta.dx.toInt();
|
||||
var dy = e.scrollDelta.dy.toInt();
|
||||
final rawDx = e.scrollDelta.dx;
|
||||
final rawDy = e.scrollDelta.dy;
|
||||
final dominantDelta = rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs();
|
||||
final isSmooth = dominantDelta < 1;
|
||||
final nowUs = DateTime.now().microsecondsSinceEpoch;
|
||||
final dtUs = _lastWheelTsUs == 0 ? 0 : nowUs - _lastWheelTsUs;
|
||||
_lastWheelTsUs = nowUs;
|
||||
int accel = 1;
|
||||
if (!isSmooth &&
|
||||
dtUs > 0 &&
|
||||
dtUs <= _wheelAccelMediumThresholdUs &&
|
||||
(isWindows || isLinux) &&
|
||||
peerPlatform == kPeerPlatformMacOS) {
|
||||
final velocity = dominantDelta / dtUs;
|
||||
if (velocity >= _wheelBurstVelocityThreshold) {
|
||||
if (dtUs < _wheelAccelFastThresholdUs) {
|
||||
accel = 3;
|
||||
} else {
|
||||
accel = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
var dx = rawDx.toInt();
|
||||
var dy = rawDy.toInt();
|
||||
if (rawDx.abs() > rawDy.abs()) {
|
||||
dy = 0;
|
||||
} else {
|
||||
dx = 0;
|
||||
}
|
||||
if (dx > 0) {
|
||||
dx = -1;
|
||||
dx = -accel;
|
||||
} else if (dx < 0) {
|
||||
dx = 1;
|
||||
dx = accel;
|
||||
}
|
||||
if (dy > 0) {
|
||||
dy = -1;
|
||||
dy = -accel;
|
||||
} else if (dy < 0) {
|
||||
dy = 1;
|
||||
dy = accel;
|
||||
}
|
||||
bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
@@ -1760,9 +1916,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;
|
||||
@@ -783,7 +784,8 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) async {
|
||||
Future<void> updateCurDisplay(SessionID sessionId,
|
||||
{updateCursorPos = false}) async {
|
||||
final newRect = displaysRect();
|
||||
if (newRect == null) {
|
||||
return;
|
||||
@@ -939,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'] ?? '';
|
||||
@@ -979,19 +1016,31 @@ class FfiModel with ChangeNotifier {
|
||||
showMsgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel}) async {
|
||||
final showNoteEdit = parent.target != null &&
|
||||
final noteAllowed = parent.target != null &&
|
||||
allowAskForNoteAtEndOfConnection(parent.target, false) &&
|
||||
(title == "Connection Error" || type == "restarting") &&
|
||||
!hasRetry;
|
||||
(title == "Connection Error" || type == "restarting");
|
||||
final showNoteEdit = noteAllowed && !hasRetry;
|
||||
if (showNoteEdit) {
|
||||
await showConnEndAuditDialogCloseCanceled(
|
||||
ffi: parent.target!, type: type, title: title, text: text);
|
||||
closeConnection();
|
||||
} else {
|
||||
VoidCallback? onSubmit;
|
||||
if (noteAllowed && hasRetry) {
|
||||
final ffi = parent.target!;
|
||||
onSubmit = () async {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
await showConnEndAuditDialogCloseCanceled(
|
||||
ffi: ffi, type: type, title: title, text: text);
|
||||
closeConnection();
|
||||
};
|
||||
}
|
||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||
hasCancel: hasCancel,
|
||||
reconnect: hasRetry ? reconnect : null,
|
||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||
reconnectTimeout: hasRetry ? _reconnects : null,
|
||||
onSubmit: onSubmit);
|
||||
}
|
||||
_timer?.cancel();
|
||||
if (hasRetry) {
|
||||
@@ -1001,6 +1050,7 @@ class FfiModel with ChangeNotifier {
|
||||
_reconnects *= 2;
|
||||
} else {
|
||||
_reconnects = 1;
|
||||
_offlineReconnectStartTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1323,6 +1373,7 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
if (displays.isNotEmpty) {
|
||||
_reconnects = 1;
|
||||
_offlineReconnectStartTime = null;
|
||||
waitForFirstImage.value = true;
|
||||
isRefreshing = false;
|
||||
}
|
||||
@@ -2113,6 +2164,9 @@ class CanvasModel with ChangeNotifier {
|
||||
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
|
||||
|
||||
Timer? _timerMobileFocusCanvasCursor;
|
||||
Timer? _timerMobileRestoreCanvasOffset;
|
||||
Offset? _offsetBeforeMobileSoftKeyboard;
|
||||
double? _scaleBeforeMobileSoftKeyboard;
|
||||
|
||||
// `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method
|
||||
// after showing the soft keyboard.
|
||||
@@ -2176,10 +2230,32 @@ class CanvasModel with ChangeNotifier {
|
||||
double w = size.width - leftToEdge - rightToEdge;
|
||||
double h = size.height - topToEdge - bottomToEdge;
|
||||
if (isMobile) {
|
||||
// Account for horizontal safe area insets on both orientations.
|
||||
w = w - mediaData.padding.left - mediaData.padding.right;
|
||||
// Vertically, subtract the bottom keyboard inset (viewInsets.bottom) and any
|
||||
// bottom overlay (e.g. key-help tools) so the canvas is not covered.
|
||||
h = h -
|
||||
mediaData.viewInsets.bottom -
|
||||
(parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ??
|
||||
0);
|
||||
// Orientation-specific handling:
|
||||
// - Portrait: additionally subtract top padding (e.g. status bar / notch)
|
||||
// - Landscape: does not subtract mediaData.padding.top/bottom (home indicator auto-hides)
|
||||
final isPortrait = size.height > size.width;
|
||||
if (isPortrait) {
|
||||
// In portrait mode, subtract the top safe-area padding (e.g. status bar / notch)
|
||||
// so the remote image is not truncated, while keeping the bottom inset to avoid
|
||||
// introducing unnecessary blank space around the canvas.
|
||||
//
|
||||
// iOS -> Android, portrait, adjust mode:
|
||||
// h = h (no padding subtracted): top and bottom are truncated
|
||||
// https://github.com/user-attachments/assets/30ed4559-c27e-432b-847f-8fec23c9f998
|
||||
// h = h - top - bottom: extra blank spaces appear
|
||||
// https://github.com/user-attachments/assets/12a98817-3b4e-43aa-be0f-4b03cf364b7e
|
||||
// h = h - top (current): works fine
|
||||
// https://github.com/user-attachments/assets/95f047f2-7f47-4a36-8113-5023989a0c81
|
||||
h = h - mediaData.padding.top;
|
||||
}
|
||||
}
|
||||
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
|
||||
}
|
||||
@@ -2578,6 +2654,9 @@ class CanvasModel with ChangeNotifier {
|
||||
_scale = 1.0;
|
||||
_lastViewStyle = ViewStyle.defaultViewStyle();
|
||||
_timerMobileFocusCanvasCursor?.cancel();
|
||||
_timerMobileRestoreCanvasOffset?.cancel();
|
||||
_offsetBeforeMobileSoftKeyboard = null;
|
||||
_scaleBeforeMobileSoftKeyboard = null;
|
||||
}
|
||||
|
||||
updateScrollPercent() {
|
||||
@@ -2606,6 +2685,31 @@ class CanvasModel with ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
void saveMobileOffsetBeforeSoftKeyboard() {
|
||||
_timerMobileRestoreCanvasOffset?.cancel();
|
||||
_offsetBeforeMobileSoftKeyboard = Offset(_x, _y);
|
||||
_scaleBeforeMobileSoftKeyboard = _scale;
|
||||
}
|
||||
|
||||
void restoreMobileOffsetAfterSoftKeyboard() {
|
||||
_timerMobileRestoreCanvasOffset?.cancel();
|
||||
_timerMobileFocusCanvasCursor?.cancel();
|
||||
final targetOffset = _offsetBeforeMobileSoftKeyboard;
|
||||
final targetScale = _scaleBeforeMobileSoftKeyboard;
|
||||
if (targetOffset == null || targetScale == null) {
|
||||
return;
|
||||
}
|
||||
_timerMobileRestoreCanvasOffset = Timer(Duration(milliseconds: 100), () {
|
||||
updateSize();
|
||||
_x = targetOffset.dx;
|
||||
_y = targetOffset.dy;
|
||||
_scale = targetScale;
|
||||
_offsetBeforeMobileSoftKeyboard = null;
|
||||
_scaleBeforeMobileSoftKeyboard = null;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
// mobile only
|
||||
// Move the canvas to make the cursor visible(center) on the screen.
|
||||
void _moveToCenterCursor() {
|
||||
@@ -2858,8 +2962,13 @@ class CursorModel with ChangeNotifier {
|
||||
_lastIsBlocked = true;
|
||||
}
|
||||
if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) {
|
||||
parent.target?.canvasModel.mobileFocusCanvasCursor();
|
||||
parent.target?.canvasModel.isMobileCanvasChanged = false;
|
||||
if (keyboardIsVisible) {
|
||||
parent.target?.canvasModel.saveMobileOffsetBeforeSoftKeyboard();
|
||||
parent.target?.canvasModel.mobileFocusCanvasCursor();
|
||||
parent.target?.canvasModel.isMobileCanvasChanged = false;
|
||||
} else {
|
||||
parent.target?.canvasModel.restoreMobileOffsetAfterSoftKeyboard();
|
||||
}
|
||||
}
|
||||
_lastKeyboardIsVisible = keyboardIsVisible;
|
||||
}
|
||||
|
||||
@@ -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,21 +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();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> setPermanentPassword(String newPW) async {
|
||||
await bind.mainSetPermanentPassword(password: newPW);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
if (newPW == pw) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// for androidUpdatekeepScreenOn only
|
||||
WakelockManager.disable(_wakelockKey);
|
||||
}
|
||||
|
||||
fetchID() async {
|
||||
@@ -613,12 +601,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 +785,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -823,6 +809,7 @@ class Client {
|
||||
bool isTerminal = false;
|
||||
String portForward = "";
|
||||
String name = "";
|
||||
String avatar = "";
|
||||
String peerId = ""; // peer user's id,show at app
|
||||
bool keyboard = false;
|
||||
bool clipboard = false;
|
||||
@@ -850,6 +837,7 @@ class Client {
|
||||
isTerminal = json['is_terminal'] ?? false;
|
||||
portForward = json['port_forward'];
|
||||
name = json['name'];
|
||||
avatar = json['avatar'] ?? '';
|
||||
peerId = json['peer_id'];
|
||||
keyboard = json['keyboard'];
|
||||
clipboard = json['clipboard'];
|
||||
@@ -873,6 +861,7 @@ class Client {
|
||||
data['is_terminal'] = isTerminal;
|
||||
data['port_forward'] = portForward;
|
||||
data['name'] = name;
|
||||
data['avatar'] = avatar;
|
||||
data['peer_id'] = peerId;
|
||||
data['keyboard'] = keyboard;
|
||||
data['clipboard'] = clipboard;
|
||||
|
||||
@@ -24,6 +24,13 @@ class TerminalModel with ChangeNotifier {
|
||||
bool _disposed = false;
|
||||
|
||||
final _inputBuffer = <String>[];
|
||||
// Buffer for output data received before terminal view has valid dimensions.
|
||||
// This prevents NaN errors when writing to terminal before layout is complete.
|
||||
final _pendingOutputChunks = <String>[];
|
||||
int _pendingOutputSize = 0;
|
||||
static const int _kMaxOutputBufferChars = 8 * 1024;
|
||||
// View ready state: true when terminal has valid dimensions, safe to write
|
||||
bool _terminalViewReady = false;
|
||||
|
||||
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
|
||||
|
||||
@@ -74,6 +81,12 @@ class TerminalModel with ChangeNotifier {
|
||||
// This piece of code must be placed before the conditional check in order to initialize properly.
|
||||
onResizeExternal?.call(w, h, pw, ph);
|
||||
|
||||
// Mark terminal view as ready and flush any buffered output on first valid resize.
|
||||
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
|
||||
if (!_terminalViewReady) {
|
||||
_markViewReady();
|
||||
}
|
||||
|
||||
if (_terminalOpened) {
|
||||
// Notify remote terminal of resize
|
||||
try {
|
||||
@@ -141,7 +154,7 @@ class TerminalModel with ChangeNotifier {
|
||||
debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e');
|
||||
// Optionally show error to user
|
||||
if (e is TimeoutException) {
|
||||
terminal.write('Failed to open terminal: Connection timeout\r\n');
|
||||
_writeToTerminal('Failed to open terminal: Connection timeout\r\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,8 +266,8 @@ class TerminalModel with ChangeNotifier {
|
||||
|
||||
void _handleTerminalOpened(Map<String, dynamic> evt) {
|
||||
final bool success = getSuccessFromEvt(evt);
|
||||
final String message = evt['message'] ?? '';
|
||||
final String? serviceId = evt['service_id'];
|
||||
final String message = evt['message']?.toString() ?? '';
|
||||
final String? serviceId = evt['service_id']?.toString();
|
||||
|
||||
debugPrint(
|
||||
'[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId');
|
||||
@@ -262,7 +275,18 @@ class TerminalModel with ChangeNotifier {
|
||||
if (success) {
|
||||
_terminalOpened = true;
|
||||
|
||||
// Service ID is now saved on the Rust side in handle_terminal_response
|
||||
// On reconnect ("Reconnected to existing terminal"), server may replay recent output.
|
||||
// If this TerminalView instance is reused (not rebuilt), duplicate lines can appear.
|
||||
// We intentionally accept this tradeoff for now to keep logic simple.
|
||||
|
||||
// Fallback: if terminal view is not yet ready but already has valid
|
||||
// dimensions (e.g. layout completed before open response arrived),
|
||||
// mark view ready now to avoid output stuck in buffer indefinitely.
|
||||
if (!_terminalViewReady &&
|
||||
terminal.viewWidth > 0 &&
|
||||
terminal.viewHeight > 0) {
|
||||
_markViewReady();
|
||||
}
|
||||
|
||||
// Process any buffered input
|
||||
_processBufferedInputAsync().then((_) {
|
||||
@@ -283,7 +307,7 @@ class TerminalModel with ChangeNotifier {
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
terminal.write('Failed to open terminal: $message\r\n');
|
||||
_writeToTerminal('Failed to open terminal: $message\r\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,29 +351,82 @@ class TerminalModel with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.write(text);
|
||||
_writeToTerminal(text);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write text to terminal, buffering if the view is not yet ready.
|
||||
/// All terminal output should go through this method to avoid NaN errors
|
||||
/// from writing before the terminal view has valid layout dimensions.
|
||||
void _writeToTerminal(String text) {
|
||||
if (!_terminalViewReady) {
|
||||
// If a single chunk exceeds the cap, keep only its tail.
|
||||
// Note: truncation may split a multi-byte ANSI escape sequence,
|
||||
// which can cause a brief visual glitch on flush. This is acceptable
|
||||
// because it only affects the pre-layout buffering window and the
|
||||
// terminal will self-correct on subsequent output.
|
||||
if (text.length >= _kMaxOutputBufferChars) {
|
||||
final truncated = text.substring(text.length - _kMaxOutputBufferChars);
|
||||
_pendingOutputChunks
|
||||
..clear()
|
||||
..add(truncated);
|
||||
_pendingOutputSize = truncated.length;
|
||||
} else {
|
||||
_pendingOutputChunks.add(text);
|
||||
_pendingOutputSize += text.length;
|
||||
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
|
||||
while (_pendingOutputSize > _kMaxOutputBufferChars &&
|
||||
_pendingOutputChunks.length > 1) {
|
||||
final removed = _pendingOutputChunks.removeAt(0);
|
||||
_pendingOutputSize -= removed.length;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
terminal.write(text);
|
||||
}
|
||||
|
||||
void _flushOutputBuffer() {
|
||||
if (_pendingOutputChunks.isEmpty) return;
|
||||
debugPrint(
|
||||
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
|
||||
for (final chunk in _pendingOutputChunks) {
|
||||
terminal.write(chunk);
|
||||
}
|
||||
_pendingOutputChunks.clear();
|
||||
_pendingOutputSize = 0;
|
||||
}
|
||||
|
||||
/// Mark terminal view as ready and flush buffered output.
|
||||
void _markViewReady() {
|
||||
if (_terminalViewReady) return;
|
||||
_terminalViewReady = true;
|
||||
_flushOutputBuffer();
|
||||
}
|
||||
|
||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||
final int exitCode = evt['exit_code'] ?? 0;
|
||||
terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||
_terminalOpened = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||
final String message = evt['message'] ?? 'Unknown error';
|
||||
terminal.write('\r\nTerminal error: $message\r\n');
|
||||
_writeToTerminal('\r\nTerminal error: $message\r\n');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
// Clear buffers to free memory
|
||||
_inputBuffer.clear();
|
||||
_pendingOutputChunks.clear();
|
||||
_pendingOutputSize = 0;
|
||||
// Terminal cleanup is handled server-side when service closes
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -16,9 +16,25 @@ bool refreshingUser = false;
|
||||
|
||||
class UserModel {
|
||||
final RxString userName = ''.obs;
|
||||
final RxString displayName = ''.obs;
|
||||
final RxString avatar = ''.obs;
|
||||
final RxBool isAdmin = false.obs;
|
||||
final RxString networkError = ''.obs;
|
||||
bool get isLogin => userName.isNotEmpty;
|
||||
String get displayNameOrUserName =>
|
||||
displayName.value.trim().isEmpty ? userName.value : displayName.value;
|
||||
String get accountLabelWithHandle {
|
||||
final username = userName.value.trim();
|
||||
if (username.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
final preferred = displayName.value.trim();
|
||||
if (preferred.isEmpty || preferred == username) {
|
||||
return username;
|
||||
}
|
||||
return '$preferred (@$username)';
|
||||
}
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
UserModel(this.parent) {
|
||||
@@ -98,7 +114,9 @@ class UserModel {
|
||||
_updateLocalUserInfo() {
|
||||
final userInfo = getLocalUserInfo();
|
||||
if (userInfo != null) {
|
||||
userName.value = userInfo['name'];
|
||||
userName.value = (userInfo['name'] ?? '').toString();
|
||||
displayName.value = (userInfo['display_name'] ?? '').toString();
|
||||
avatar.value = (userInfo['avatar'] ?? '').toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +128,14 @@ class UserModel {
|
||||
await gFFI.groupModel.reset();
|
||||
}
|
||||
userName.value = '';
|
||||
displayName.value = '';
|
||||
avatar.value = '';
|
||||
}
|
||||
|
||||
_parseAndUpdateUser(UserPayload user) {
|
||||
userName.value = user.name;
|
||||
displayName.value = user.displayName;
|
||||
avatar.value = user.avatar;
|
||||
isAdmin.value = user.isAdmin;
|
||||
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
|
||||
if (isWeb) {
|
||||
|
||||
@@ -1159,10 +1159,6 @@ class RustdeskImpl {
|
||||
return Future.value('');
|
||||
}
|
||||
|
||||
Future<String> mainGetPermanentPassword({dynamic hint}) {
|
||||
return Future.value('');
|
||||
}
|
||||
|
||||
Future<String> mainGetFingerprint({dynamic hint}) {
|
||||
return Future.value('');
|
||||
}
|
||||
@@ -1346,9 +1342,9 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("mainUpdateTemporaryPassword");
|
||||
}
|
||||
|
||||
Future<void> mainSetPermanentPassword(
|
||||
Future<bool> mainSetPermanentPasswordWithResult(
|
||||
{required String password, dynamic hint}) {
|
||||
throw UnimplementedError("mainSetPermanentPassword");
|
||||
throw UnimplementedError("mainSetPermanentPasswordWithResult");
|
||||
}
|
||||
|
||||
Future<bool> mainCheckSuperUserPermission({dynamic hint}) {
|
||||
@@ -2034,5 +2030,9 @@ class RustdeskImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(runner LANGUAGES CXX)
|
||||
project(runner LANGUAGES C CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
@@ -54,6 +54,55 @@ add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
# Wayland protocol for keyboard shortcuts inhibit
|
||||
pkg_check_modules(WAYLAND_CLIENT IMPORTED_TARGET wayland-client)
|
||||
pkg_check_modules(WAYLAND_PROTOCOLS_PKG QUIET wayland-protocols)
|
||||
pkg_check_modules(WAYLAND_SCANNER_PKG QUIET wayland-scanner)
|
||||
|
||||
if(WAYLAND_PROTOCOLS_PKG_FOUND)
|
||||
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
||||
endif()
|
||||
if(WAYLAND_SCANNER_PKG_FOUND)
|
||||
pkg_get_variable(WAYLAND_SCANNER wayland-scanner wayland_scanner)
|
||||
endif()
|
||||
|
||||
if(WAYLAND_CLIENT_FOUND AND WAYLAND_PROTOCOLS_DIR AND WAYLAND_SCANNER)
|
||||
set(KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL
|
||||
"${WAYLAND_PROTOCOLS_DIR}/unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml")
|
||||
|
||||
if(EXISTS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL})
|
||||
set(WAYLAND_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/wayland-protocols")
|
||||
file(MAKE_DIRECTORY ${WAYLAND_GENERATED_DIR})
|
||||
|
||||
# Generate client header
|
||||
add_custom_command(
|
||||
OUTPUT "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
|
||||
COMMAND ${WAYLAND_SCANNER} client-header
|
||||
${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}
|
||||
"${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
|
||||
DEPENDS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# Generate protocol code
|
||||
add_custom_command(
|
||||
OUTPUT "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c"
|
||||
COMMAND ${WAYLAND_SCANNER} private-code
|
||||
${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}
|
||||
"${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c"
|
||||
DEPENDS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set(WAYLAND_PROTOCOL_SOURCES
|
||||
"${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
|
||||
"${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c"
|
||||
)
|
||||
|
||||
set(HAS_KEYBOARD_SHORTCUTS_INHIBIT TRUE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME above,
|
||||
@@ -63,9 +112,11 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"wayland_shortcuts_inhibit.cc"
|
||||
"bump_mouse.cc"
|
||||
"bump_mouse_x11.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
${WAYLAND_PROTOCOL_SOURCES}
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
@@ -78,6 +129,13 @@ target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS})
|
||||
# target_link_libraries(${BINARY_NAME} PRIVATE librustdesk)
|
||||
|
||||
# Wayland support for keyboard shortcuts inhibit
|
||||
if(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||
target_include_directories(${BINARY_NAME} PRIVATE ${WAYLAND_GENERATED_DIR})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::WAYLAND_CLIENT)
|
||||
endif()
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||
#include "wayland_shortcuts_inhibit.h"
|
||||
#endif
|
||||
|
||||
#include <desktop_multi_window/desktop_multi_window_plugin.h>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
@@ -91,6 +96,13 @@ static void my_application_activate(GApplication* application) {
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
|
||||
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||
// Register callback for sub-windows created by desktop_multi_window plugin
|
||||
// Only sub-windows (remote windows) need keyboard shortcuts inhibition
|
||||
desktop_multi_window_plugin_set_window_created_callback(
|
||||
(WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow);
|
||||
#endif
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||
|
||||
244
flutter/linux/wayland_shortcuts_inhibit.cc
Normal file
244
flutter/linux/wayland_shortcuts_inhibit.cc
Normal file
@@ -0,0 +1,244 @@
|
||||
// Wayland keyboard shortcuts inhibit implementation
|
||||
// Uses the zwp_keyboard_shortcuts_inhibit_manager_v1 protocol to request
|
||||
// the compositor to disable system shortcuts for specific windows.
|
||||
|
||||
#include "wayland_shortcuts_inhibit.h"
|
||||
|
||||
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||
|
||||
#include <cstring>
|
||||
#include <gdk/gdkwayland.h>
|
||||
#include <wayland-client.h>
|
||||
#include "keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
|
||||
|
||||
// Data structure to hold inhibitor state for each window
|
||||
typedef struct {
|
||||
struct zwp_keyboard_shortcuts_inhibit_manager_v1* manager;
|
||||
struct zwp_keyboard_shortcuts_inhibitor_v1* inhibitor;
|
||||
} ShortcutsInhibitData;
|
||||
|
||||
// Cleanup function for ShortcutsInhibitData
|
||||
static void shortcuts_inhibit_data_free(gpointer data) {
|
||||
ShortcutsInhibitData* inhibit_data = static_cast<ShortcutsInhibitData*>(data);
|
||||
if (inhibit_data->inhibitor != NULL) {
|
||||
zwp_keyboard_shortcuts_inhibitor_v1_destroy(inhibit_data->inhibitor);
|
||||
}
|
||||
if (inhibit_data->manager != NULL) {
|
||||
zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(inhibit_data->manager);
|
||||
}
|
||||
g_free(inhibit_data);
|
||||
}
|
||||
|
||||
// Wayland registry handler to find the shortcuts inhibit manager
|
||||
static void registry_handle_global(void* data, struct wl_registry* registry,
|
||||
uint32_t name, const char* interface,
|
||||
uint32_t /*version*/) {
|
||||
ShortcutsInhibitData* inhibit_data = static_cast<ShortcutsInhibitData*>(data);
|
||||
if (strcmp(interface,
|
||||
zwp_keyboard_shortcuts_inhibit_manager_v1_interface.name) == 0) {
|
||||
inhibit_data->manager =
|
||||
static_cast<zwp_keyboard_shortcuts_inhibit_manager_v1*>(wl_registry_bind(
|
||||
registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface,
|
||||
1));
|
||||
}
|
||||
}
|
||||
|
||||
static void registry_handle_global_remove(void* /*data*/, struct wl_registry* /*registry*/,
|
||||
uint32_t /*name*/) {
|
||||
// Not needed for this use case
|
||||
}
|
||||
|
||||
static const struct wl_registry_listener registry_listener = {
|
||||
registry_handle_global,
|
||||
registry_handle_global_remove,
|
||||
};
|
||||
|
||||
// Inhibitor event handlers
|
||||
static void inhibitor_active(void* /*data*/,
|
||||
struct zwp_keyboard_shortcuts_inhibitor_v1* /*inhibitor*/) {
|
||||
// Inhibitor is now active, shortcuts are being captured
|
||||
}
|
||||
|
||||
static void inhibitor_inactive(void* /*data*/,
|
||||
struct zwp_keyboard_shortcuts_inhibitor_v1* /*inhibitor*/) {
|
||||
// Inhibitor is now inactive, shortcuts restored to compositor
|
||||
}
|
||||
|
||||
static const struct zwp_keyboard_shortcuts_inhibitor_v1_listener inhibitor_listener = {
|
||||
inhibitor_active,
|
||||
inhibitor_inactive,
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
static void uninhibit_keyboard_shortcuts(GtkWindow* window);
|
||||
|
||||
// Inhibit keyboard shortcuts on Wayland for a specific window
|
||||
static void inhibit_keyboard_shortcuts(GtkWindow* window) {
|
||||
GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(window));
|
||||
if (!GDK_IS_WAYLAND_DISPLAY(display)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already inhibited for this window
|
||||
if (g_object_get_data(G_OBJECT(window), "shortcuts-inhibit-data") != NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShortcutsInhibitData* inhibit_data = g_new0(ShortcutsInhibitData, 1);
|
||||
|
||||
struct wl_display* wl_display = gdk_wayland_display_get_wl_display(display);
|
||||
if (wl_display == NULL) {
|
||||
shortcuts_inhibit_data_free(inhibit_data);
|
||||
return;
|
||||
}
|
||||
|
||||
struct wl_registry* registry = wl_display_get_registry(wl_display);
|
||||
if (registry == NULL) {
|
||||
shortcuts_inhibit_data_free(inhibit_data);
|
||||
return;
|
||||
}
|
||||
|
||||
wl_registry_add_listener(registry, ®istry_listener, inhibit_data);
|
||||
wl_display_roundtrip(wl_display);
|
||||
|
||||
if (inhibit_data->manager == NULL) {
|
||||
wl_registry_destroy(registry);
|
||||
shortcuts_inhibit_data_free(inhibit_data);
|
||||
return;
|
||||
}
|
||||
|
||||
GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
|
||||
if (gdk_window == NULL) {
|
||||
wl_registry_destroy(registry);
|
||||
shortcuts_inhibit_data_free(inhibit_data);
|
||||
return;
|
||||
}
|
||||
|
||||
struct wl_surface* surface = gdk_wayland_window_get_wl_surface(gdk_window);
|
||||
if (surface == NULL) {
|
||||
wl_registry_destroy(registry);
|
||||
shortcuts_inhibit_data_free(inhibit_data);
|
||||
return;
|
||||
}
|
||||
|
||||
GdkSeat* gdk_seat = gdk_display_get_default_seat(display);
|
||||
if (gdk_seat == NULL) {
|
||||
wl_registry_destroy(registry);
|
||||
shortcuts_inhibit_data_free(inhibit_data);
|
||||
return;
|
||||
}
|
||||
|
||||
struct wl_seat* seat = gdk_wayland_seat_get_wl_seat(gdk_seat);
|
||||
if (seat == NULL) {
|
||||
wl_registry_destroy(registry);
|
||||
shortcuts_inhibit_data_free(inhibit_data);
|
||||
return;
|
||||
}
|
||||
|
||||
inhibit_data->inhibitor =
|
||||
zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
|
||||
inhibit_data->manager, surface, seat);
|
||||
|
||||
if (inhibit_data->inhibitor == NULL) {
|
||||
wl_registry_destroy(registry);
|
||||
shortcuts_inhibit_data_free(inhibit_data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add listener to monitor active/inactive state
|
||||
zwp_keyboard_shortcuts_inhibitor_v1_add_listener(
|
||||
inhibit_data->inhibitor, &inhibitor_listener, window);
|
||||
|
||||
wl_display_roundtrip(wl_display);
|
||||
wl_registry_destroy(registry);
|
||||
|
||||
// Associate the inhibit data with the window for cleanup on destroy
|
||||
g_object_set_data_full(G_OBJECT(window), "shortcuts-inhibit-data",
|
||||
inhibit_data, shortcuts_inhibit_data_free);
|
||||
}
|
||||
|
||||
// Remove keyboard shortcuts inhibitor from a window
|
||||
static void uninhibit_keyboard_shortcuts(GtkWindow* window) {
|
||||
ShortcutsInhibitData* inhibit_data = static_cast<ShortcutsInhibitData*>(
|
||||
g_object_get_data(G_OBJECT(window), "shortcuts-inhibit-data"));
|
||||
|
||||
if (inhibit_data == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This will trigger shortcuts_inhibit_data_free via g_object_set_data
|
||||
g_object_set_data(G_OBJECT(window), "shortcuts-inhibit-data", NULL);
|
||||
}
|
||||
|
||||
// Focus event handlers for dynamic inhibitor management
|
||||
static gboolean on_window_focus_in(GtkWidget* widget, GdkEventFocus* /*event*/, gpointer /*user_data*/) {
|
||||
if (GTK_IS_WINDOW(widget)) {
|
||||
inhibit_keyboard_shortcuts(GTK_WINDOW(widget));
|
||||
}
|
||||
return FALSE; // Continue event propagation
|
||||
}
|
||||
|
||||
static gboolean on_window_focus_out(GtkWidget* widget, GdkEventFocus* /*event*/, gpointer /*user_data*/) {
|
||||
if (GTK_IS_WINDOW(widget)) {
|
||||
uninhibit_keyboard_shortcuts(GTK_WINDOW(widget));
|
||||
}
|
||||
return FALSE; // Continue event propagation
|
||||
}
|
||||
|
||||
// Key for marking window as having focus handlers connected
|
||||
static const char* const kFocusHandlersConnectedKey = "shortcuts-inhibit-focus-handlers-connected";
|
||||
// Key for marking window as having a pending realize handler
|
||||
static const char* const kRealizeHandlerConnectedKey = "shortcuts-inhibit-realize-handler-connected";
|
||||
|
||||
// Callback when window is realized (mapped to screen)
|
||||
// Sets up focus-based inhibitor management
|
||||
static void on_window_realize(GtkWidget* widget, gpointer /*user_data*/) {
|
||||
if (GTK_IS_WINDOW(widget)) {
|
||||
// Check if focus handlers are already connected to avoid duplicates
|
||||
if (g_object_get_data(G_OBJECT(widget), kFocusHandlersConnectedKey) != NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect focus events for dynamic inhibitor management
|
||||
g_signal_connect(widget, "focus-in-event",
|
||||
G_CALLBACK(on_window_focus_in), NULL);
|
||||
g_signal_connect(widget, "focus-out-event",
|
||||
G_CALLBACK(on_window_focus_out), NULL);
|
||||
|
||||
// Mark as connected to prevent duplicate connections
|
||||
g_object_set_data(G_OBJECT(widget), kFocusHandlersConnectedKey, GINT_TO_POINTER(1));
|
||||
|
||||
// If window already has focus, create inhibitor now
|
||||
if (gtk_window_has_toplevel_focus(GTK_WINDOW(widget))) {
|
||||
inhibit_keyboard_shortcuts(GTK_WINDOW(widget));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public API: Initialize shortcuts inhibit for a sub-window
|
||||
void wayland_shortcuts_inhibit_init_for_subwindow(void* view) {
|
||||
GtkWidget* widget = GTK_WIDGET(view);
|
||||
GtkWidget* toplevel = gtk_widget_get_toplevel(widget);
|
||||
|
||||
if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) {
|
||||
// Check if already initialized to avoid duplicate realize handlers
|
||||
if (g_object_get_data(G_OBJECT(toplevel), kFocusHandlersConnectedKey) != NULL ||
|
||||
g_object_get_data(G_OBJECT(toplevel), kRealizeHandlerConnectedKey) != NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gtk_widget_get_realized(toplevel)) {
|
||||
// Window is already realized, set up focus handlers now
|
||||
on_window_realize(toplevel, NULL);
|
||||
} else {
|
||||
// Mark realize handler as connected to prevent duplicate connections
|
||||
// if called again before window is realized
|
||||
g_object_set_data(G_OBJECT(toplevel), kRealizeHandlerConnectedKey, GINT_TO_POINTER(1));
|
||||
// Wait for window to be realized
|
||||
g_signal_connect(toplevel, "realize",
|
||||
G_CALLBACK(on_window_realize), NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||
22
flutter/linux/wayland_shortcuts_inhibit.h
Normal file
22
flutter/linux/wayland_shortcuts_inhibit.h
Normal file
@@ -0,0 +1,22 @@
|
||||
// Wayland keyboard shortcuts inhibit support
|
||||
// This module provides functionality to inhibit system keyboard shortcuts
|
||||
// on Wayland compositors, allowing remote desktop windows to capture all
|
||||
// key events including Super, Alt+Tab, etc.
|
||||
|
||||
#ifndef WAYLAND_SHORTCUTS_INHIBIT_H_
|
||||
#define WAYLAND_SHORTCUTS_INHIBIT_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||
|
||||
// Initialize shortcuts inhibit for a sub-window created by desktop_multi_window plugin.
|
||||
// This sets up focus-based inhibitor management: inhibitor is created when
|
||||
// the window gains focus and destroyed when it loses focus.
|
||||
//
|
||||
// @param view The FlView of the sub-window
|
||||
void wayland_shortcuts_inhibit_init_for_subwindow(void* view);
|
||||
|
||||
#endif // defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
|
||||
|
||||
#endif // WAYLAND_SHORTCUTS_INHIBIT_H_
|
||||
@@ -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.5+63
|
||||
version: 1.4.6+64
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cstdlib> // for getenv and _putenv
|
||||
#include <cstring> // for strcmp
|
||||
#include <string> // for std::wstring
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -15,6 +16,43 @@ constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
// Static variable to hold the custom icon (needs cleanup on exit)
|
||||
static HICON g_custom_icon_ = nullptr;
|
||||
|
||||
// Try to load icon from data\flutter_assets\assets\icon.ico if it exists.
|
||||
// Returns nullptr if the file doesn't exist or can't be loaded.
|
||||
HICON LoadCustomIcon() {
|
||||
if (g_custom_icon_ != nullptr) {
|
||||
return g_custom_icon_;
|
||||
}
|
||||
wchar_t exe_path[MAX_PATH];
|
||||
if (!GetModuleFileNameW(nullptr, exe_path, MAX_PATH)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::wstring icon_path = exe_path;
|
||||
size_t last_slash = icon_path.find_last_of(L"\\/");
|
||||
if (last_slash == std::wstring::npos) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
icon_path = icon_path.substr(0, last_slash + 1);
|
||||
icon_path += L"data\\flutter_assets\\assets\\icon.ico";
|
||||
|
||||
// Check file attributes - reject if missing, directory, or reparse point (symlink/junction)
|
||||
DWORD file_attr = GetFileAttributesW(icon_path.c_str());
|
||||
if (file_attr == INVALID_FILE_ATTRIBUTES ||
|
||||
(file_attr & FILE_ATTRIBUTE_DIRECTORY) ||
|
||||
(file_attr & FILE_ATTRIBUTE_REPARSE_POINT)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
g_custom_icon_ = (HICON)LoadImageW(
|
||||
nullptr, icon_path.c_str(), IMAGE_ICON, 0, 0,
|
||||
LR_LOADFROMFILE | LR_DEFAULTSIZE);
|
||||
return g_custom_icon_;
|
||||
}
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
@@ -81,8 +119,16 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
window_class.cbClsExtra = 0;
|
||||
window_class.cbWndExtra = 0;
|
||||
window_class.hInstance = GetModuleHandle(nullptr);
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
|
||||
// Try to load icon from data\flutter_assets\assets\icon.ico if it exists
|
||||
HICON custom_icon = LoadCustomIcon();
|
||||
if (custom_icon != nullptr) {
|
||||
window_class.hIcon = custom_icon;
|
||||
} else {
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
}
|
||||
|
||||
window_class.hbrBackground = 0;
|
||||
window_class.lpszMenuName = nullptr;
|
||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||
@@ -95,6 +141,12 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
|
||||
// Clean up the custom icon if it was loaded
|
||||
if (g_custom_icon_ != nullptr) {
|
||||
DestroyIcon(g_custom_icon_);
|
||||
g_custom_icon_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window::Win32Window() {
|
||||
|
||||
@@ -10,7 +10,7 @@ TODO: Move this lib to a separate project.
|
||||
|
||||
## How it works
|
||||
|
||||
Terminalogies:
|
||||
Terminologies:
|
||||
|
||||
- cliprdr: this module
|
||||
- local: the endpoint which initiates a file copy events
|
||||
@@ -50,7 +50,7 @@ sequenceDiagram
|
||||
r ->> l: Format List Response (notified)
|
||||
r ->> l: Format Data Request (requests file list)
|
||||
activate l
|
||||
note left of l: Retrive file list from system clipboard
|
||||
note left of l: Retrieve file list from system clipboard
|
||||
l ->> r: Format Data Response (containing file list)
|
||||
deactivate l
|
||||
note over r: Update system clipboard with received file list
|
||||
@@ -84,10 +84,10 @@ and copy files to remote.
|
||||
The protocol was originally designed as an extension of the Windows RDP,
|
||||
so the specific message packages fits windows well.
|
||||
|
||||
When starting cliprdr, a thread is spawn to create a invisible window
|
||||
When starting cliprdr, a thread is spawned to create an invisible window
|
||||
and to subscribe to OLE clipboard events.
|
||||
The window's callback (see `cliprdr_proc` in `src/windows/wf_cliprdr.c`) was
|
||||
set to handle a variaty of events.
|
||||
set to handle a variety of events.
|
||||
|
||||
Detailed implementation is shown in pictures above.
|
||||
|
||||
@@ -108,18 +108,18 @@ after filtering out those pointing to our FUSE directory or duplicated,
|
||||
send format list directly to remote.
|
||||
|
||||
The cliprdr server also uses clipboard client for setting clipboard,
|
||||
or retrive paths from system.
|
||||
or retrieve paths from system.
|
||||
|
||||
#### Local File List
|
||||
|
||||
The local file list is a temperary list of file metadata.
|
||||
The local file list is a temporary list of file metadata.
|
||||
When receiving file contents PDU from peer, the server picks
|
||||
out the file requested and open it for reading if necessary.
|
||||
|
||||
Also when receiving Format Data Request PDU from remote asking for file list,
|
||||
the local file list should be rebuilt from file list retrieved from Clipboard Client.
|
||||
|
||||
Some caching and preloading could done on it since applications are likely to read
|
||||
Some caching and preloading could be done on it since applications are likely to read
|
||||
on the list sequentially.
|
||||
|
||||
#### FUSE server
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -261,6 +261,8 @@ impl KeyboardControllable for Enigo {
|
||||
} else {
|
||||
if let Some(keyboard) = &mut self.custom_keyboard {
|
||||
keyboard.key_sequence(sequence)
|
||||
} else {
|
||||
log::warn!("Enigo::key_sequence: no custom_keyboard set for Wayland!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,6 +279,7 @@ impl KeyboardControllable for Enigo {
|
||||
if let Some(keyboard) = &mut self.custom_keyboard {
|
||||
keyboard.key_down(key)
|
||||
} else {
|
||||
log::warn!("Enigo::key_down: no custom_keyboard set for Wayland!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -290,13 +293,24 @@ impl KeyboardControllable for Enigo {
|
||||
} else {
|
||||
if let Some(keyboard) = &mut self.custom_keyboard {
|
||||
keyboard.key_up(key)
|
||||
} else {
|
||||
log::warn!("Enigo::key_up: no custom_keyboard set for Wayland!");
|
||||
}
|
||||
}
|
||||
}
|
||||
fn key_click(&mut self, key: Key) {
|
||||
if self.tfc_key_click(key).is_err() {
|
||||
self.key_down(key).ok();
|
||||
self.key_up(key);
|
||||
if self.is_x11 {
|
||||
// X11: try tfc first, then fallback to key_down/key_up
|
||||
if self.tfc_key_click(key).is_err() {
|
||||
self.key_down(key).ok();
|
||||
self.key_up(key);
|
||||
}
|
||||
} else {
|
||||
if let Some(keyboard) = &mut self.custom_keyboard {
|
||||
keyboard.key_click(key);
|
||||
} else {
|
||||
log::warn!("Enigo::key_click: no custom_keyboard set for Wayland!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ impl KeyboardControllable for Enigo {
|
||||
for pos in 0..mod_len {
|
||||
let rpos = mod_len - 1 - pos;
|
||||
if flag & (0x0001 << rpos) != 0 {
|
||||
self.key_up(modifiers[pos]);
|
||||
self.key_up(modifiers[rpos]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +298,18 @@ impl KeyboardControllable for Enigo {
|
||||
}
|
||||
|
||||
fn key_up(&mut self, key: Key) {
|
||||
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
|
||||
match key {
|
||||
Key::Layout(c) => {
|
||||
let code = self.get_layoutdependent_keycode(c);
|
||||
if code as u16 != 0xFFFF {
|
||||
let vk = code & 0x00FF;
|
||||
keybd_event(KEYEVENTF_KEYUP, vk, 0);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key_state(&mut self, key: Key) -> bool {
|
||||
|
||||
Submodule libs/hbb_common updated: 073403edbf...f08ce5d6d0
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.5"
|
||||
version = "1.4.6"
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error>
|
||||
let mut e: *mut xcb_generic_error_t = std::ptr::null_mut();
|
||||
let reply = xcb_shm_query_version_reply(c, cookie, &mut e as _);
|
||||
if reply.is_null() {
|
||||
// TODO: Should seperate SHM disabled from SHM not supported?
|
||||
// TODO: Should separate SHM disabled from SHM not supported?
|
||||
return Err(Error::UnsupportedExtension);
|
||||
} else {
|
||||
// https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229
|
||||
|
||||
@@ -6,15 +6,13 @@ if [ "$1" = configure ]; then
|
||||
|
||||
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
|
||||
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
|
||||
|
||||
|
||||
if [ "systemd" == "$INITSYS" ]; then
|
||||
|
||||
if [ -e /etc/systemd/system/rustdesk.service ]; then
|
||||
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
|
||||
fi
|
||||
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
|
||||
parsedVersion=$(echo "${version//./}")
|
||||
mkdir -p /usr/lib/systemd/system/
|
||||
mkdir -p /usr/lib/systemd/system/
|
||||
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
|
||||
# try fix error in Ubuntu 18.04
|
||||
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.4.5
|
||||
pkgver=1.4.6
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -31,22 +31,168 @@ LExit:
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
// CAUTION: We can't simply remove the install folder here, because silent repair/upgrade will fail.
|
||||
// `RemoveInstallFolder()` is a deferred custom action, it will be executed after the files are copied.
|
||||
// `msiexec /i package.msi /qn`
|
||||
// Helper function to safely delete a file or directory using handle-based deletion.
|
||||
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
|
||||
BOOL SafeDeleteItem(LPCWSTR fullPath)
|
||||
{
|
||||
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
|
||||
// to prevent following symlinks.
|
||||
// Use shared access to allow deletion even when other processes have the file open.
|
||||
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
|
||||
HANDLE hFile = CreateFileW(
|
||||
fullPath,
|
||||
DELETE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
flags,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to open '%ls'. Error: %lu", fullPath, GetLastError());
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Use SetFileInformationByHandle to mark for deletion.
|
||||
// The file will be deleted when the handle is closed.
|
||||
FILE_DISPOSITION_INFO dispInfo;
|
||||
dispInfo.DeleteFile = TRUE;
|
||||
|
||||
BOOL result = SetFileInformationByHandle(
|
||||
hFile,
|
||||
FileDispositionInfo,
|
||||
&dispInfo,
|
||||
sizeof(dispInfo)
|
||||
);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to mark '%ls' for deletion. Error: %lu", fullPath, error);
|
||||
}
|
||||
|
||||
CloseHandle(hFile);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper function to recursively delete a directory's contents with detailed logging.
|
||||
void RecursiveDelete(LPCWSTR path)
|
||||
{
|
||||
// Ensure the path is not empty or null.
|
||||
if (path == NULL || path[0] == L'\0')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Extra safety: never operate directly on a root path.
|
||||
if (PathIsRootW(path))
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
|
||||
return;
|
||||
}
|
||||
|
||||
// MAX_PATH is enough here since the installer should not be using longer paths.
|
||||
// No need to handle extended-length paths (\\?\) in this context.
|
||||
WCHAR searchPath[MAX_PATH];
|
||||
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
|
||||
if (FAILED(hr)) {
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
|
||||
return;
|
||||
}
|
||||
|
||||
WIN32_FIND_DATAW findData;
|
||||
HANDLE hFind = FindFirstFileW(searchPath, &findData);
|
||||
|
||||
if (hFind == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Skip '.' and '..' directories.
|
||||
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// MAX_PATH is enough here since the installer should not be using longer paths.
|
||||
// No need to handle extended-length paths (\\?\) in this context.
|
||||
WCHAR fullPath[MAX_PATH];
|
||||
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
|
||||
if (FAILED(hr)) {
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Before acting, ensure the read-only attribute is not set.
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
|
||||
{
|
||||
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
{
|
||||
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
|
||||
// Do not follow reparse points, only remove the link itself.
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recursively delete directory contents first
|
||||
RecursiveDelete(fullPath);
|
||||
// Then delete the directory itself
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Delete file using safe handle-based deletion
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
} while (FindNextFileW(hFind, &findData) != 0);
|
||||
|
||||
DWORD lastError = GetLastError();
|
||||
if (lastError != ERROR_NO_MORE_FILES)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
|
||||
}
|
||||
|
||||
FindClose(hFind);
|
||||
}
|
||||
|
||||
// See `Package.wxs` for the sequence of this custom action.
|
||||
//
|
||||
// So we need to delete the files separately in install folder.
|
||||
// Upgrade/uninstall sequence:
|
||||
// 1. InstallInitialize
|
||||
// 2. RemoveExistingProducts
|
||||
// ├─ TerminateProcesses
|
||||
// ├─ TryStopDeleteService
|
||||
// ├─ RemoveInstallFolder - <-- Here
|
||||
// └─ RemoveFiles
|
||||
// 3. InstallValidate
|
||||
// 4. InstallFiles
|
||||
// 5. InstallExecute
|
||||
// 6. InstallFinalize
|
||||
UINT __stdcall RemoveInstallFolder(
|
||||
__in MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
DWORD er = ERROR_SUCCESS;
|
||||
|
||||
int nResult = 0;
|
||||
LPWSTR installFolder = NULL;
|
||||
LPWSTR pwz = NULL;
|
||||
LPWSTR pwzData = NULL;
|
||||
WCHAR runtimeBroker[1024] = { 0, };
|
||||
|
||||
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
|
||||
ExitOnFailure(hr, "Failed to initialize");
|
||||
@@ -58,24 +204,23 @@ UINT __stdcall RemoveInstallFolder(
|
||||
hr = WcaReadStringFromCaData(&pwz, &installFolder);
|
||||
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
|
||||
|
||||
StringCchPrintfW(runtimeBroker, sizeof(runtimeBroker) / sizeof(runtimeBroker[0]), L"%ls\\RuntimeBroker_rustdesk.exe", installFolder);
|
||||
|
||||
SHFILEOPSTRUCTW fileOp;
|
||||
ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT));
|
||||
fileOp.wFunc = FO_DELETE;
|
||||
fileOp.pFrom = runtimeBroker;
|
||||
fileOp.fFlags = FOF_NOCONFIRMATION | FOF_SILENT;
|
||||
|
||||
nResult = SHFileOperationW(&fileOp);
|
||||
if (nResult == 0)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has been deleted.", runtimeBroker);
|
||||
if (installFolder == NULL || installFolder[0] == L'\0') {
|
||||
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
|
||||
goto LExit;
|
||||
}
|
||||
else
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", runtimeBroker, nResult);
|
||||
|
||||
if (PathIsRootW(installFolder)) {
|
||||
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
|
||||
goto LExit;
|
||||
}
|
||||
|
||||
WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
|
||||
|
||||
RecursiveDelete(installFolder);
|
||||
|
||||
// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
|
||||
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
|
||||
|
||||
LExit:
|
||||
ReleaseStr(pwzData);
|
||||
|
||||
@@ -109,9 +254,12 @@ bool TerminateProcessIfNotContainsParam(pfnNtQueryInformationProcess NtQueryInfo
|
||||
{
|
||||
if (pebUpp.CommandLine.Length > 0)
|
||||
{
|
||||
WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length);
|
||||
// Allocate extra space for null terminator
|
||||
WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length + sizeof(WCHAR));
|
||||
if (commandLine != NULL)
|
||||
{
|
||||
// Initialize all bytes to zero for safety
|
||||
memset(commandLine, 0, pebUpp.CommandLine.Length + sizeof(WCHAR));
|
||||
if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer,
|
||||
commandLine, pebUpp.CommandLine.Length, &dwBytesRead))
|
||||
{
|
||||
|
||||
@@ -336,7 +336,9 @@ def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
|
||||
f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
|
||||
)
|
||||
|
||||
estimated_size = get_folder_size(dist_dir)
|
||||
# EstimatedSize in uninstall registry must be in KB.
|
||||
estimated_size_bytes = get_folder_size(dist_dir)
|
||||
estimated_size = max(1, (estimated_size_bytes + 1023) // 1024)
|
||||
lines_new.append(
|
||||
f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.5
|
||||
Version: 1.4.6
|
||||
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.5
|
||||
Version: 1.4.6
|
||||
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.5
|
||||
Version: 1.4.6
|
||||
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/
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::{
|
||||
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
|
||||
kcp_stream::KcpStream,
|
||||
secure_tcp,
|
||||
ui_interface::{get_builtin_option, use_texture_render},
|
||||
ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
@@ -119,10 +119,13 @@ pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access";
|
||||
pub const LOGIN_MSG_OFFLINE: &str = "Offline";
|
||||
pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported";
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version.";
|
||||
pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "ubuntu-21-04-required";
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str =
|
||||
"Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.";
|
||||
"wayland-requires-higher-linux-version";
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const SCRAP_XDP_PORTAL_UNAVAILABLE: &str =
|
||||
"xdp-portal-unavailable";
|
||||
pub const SCRAP_X11_REQUIRED: &str = "x11 expected";
|
||||
pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
|
||||
|
||||
@@ -2625,15 +2628,32 @@ impl LoginConfigHandler {
|
||||
} else {
|
||||
(my_id, self.id.clone())
|
||||
};
|
||||
let mut avatar = get_builtin_option(keys::OPTION_AVATAR);
|
||||
if avatar.is_empty() {
|
||||
avatar = serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option(
|
||||
"user_info",
|
||||
))
|
||||
.ok()
|
||||
.and_then(|x| {
|
||||
x.get("avatar")
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|x| x.trim().to_owned())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
avatar = resolve_avatar_url(avatar);
|
||||
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
|
||||
if display_name.is_empty() {
|
||||
display_name =
|
||||
serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option("user_info"))
|
||||
.map(|x| {
|
||||
x.get("name")
|
||||
.map(|x| x.as_str().unwrap_or_default())
|
||||
x.get("display_name")
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|x| x.trim())
|
||||
.filter(|x| !x.is_empty())
|
||||
.or_else(|| x.get("name").and_then(|x| x.as_str()))
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or_default()
|
||||
.to_owned()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
@@ -2681,6 +2701,7 @@ impl LoginConfigHandler {
|
||||
})
|
||||
.into(),
|
||||
hwid,
|
||||
avatar,
|
||||
..Default::default()
|
||||
};
|
||||
match self.conn_type {
|
||||
|
||||
@@ -197,7 +197,7 @@ pub fn check_clipboard_cm() -> ResultType<MultiClipboards> {
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
let to_update_data = proto::from_multi_clipbards(multi_clipboards);
|
||||
let to_update_data = proto::from_multi_clipboards(multi_clipboards);
|
||||
if to_update_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -432,7 +432,7 @@ impl ClipboardContext {
|
||||
#[cfg(target_os = "macos")]
|
||||
let is_kde_x11 = false;
|
||||
let clear_holder_text = if is_kde_x11 {
|
||||
"RustDesk placeholder to clear the file clipbard"
|
||||
"RustDesk placeholder to clear the file clipboard"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -672,7 +672,7 @@ mod proto {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn from_multi_clipbards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
|
||||
pub fn from_multi_clipboards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
|
||||
multi_clipboards
|
||||
.into_iter()
|
||||
.filter_map(from_clipboard)
|
||||
@@ -814,7 +814,7 @@ pub mod clipboard_listener {
|
||||
subscribers: listener_lock.subscribers.clone(),
|
||||
};
|
||||
let (tx_start_res, rx_start_res) = channel();
|
||||
let h = start_clipbard_master_thread(handler, tx_start_res);
|
||||
let h = start_clipboard_master_thread(handler, tx_start_res);
|
||||
let shutdown = match rx_start_res.recv() {
|
||||
Ok((Some(s), _)) => s,
|
||||
Ok((None, err)) => {
|
||||
@@ -854,7 +854,7 @@ pub mod clipboard_listener {
|
||||
log::info!("Clipboard listener unsubscribed: {}", name);
|
||||
}
|
||||
|
||||
fn start_clipbard_master_thread(
|
||||
fn start_clipboard_master_thread(
|
||||
handler: impl ClipboardHandler + Send + 'static,
|
||||
tx_start_res: Sender<(Option<Shutdown>, String)>,
|
||||
) -> JoinHandle<()> {
|
||||
|
||||
595
src/common.rs
595
src/common.rs
@@ -39,7 +39,7 @@ use hbb_common::{
|
||||
|
||||
use crate::{
|
||||
hbbs_http::{create_http_client_async, get_url_for_tls},
|
||||
ui_interface::{get_option, set_option},
|
||||
ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option},
|
||||
};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@@ -1072,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);
|
||||
@@ -1090,6 +1086,7 @@ fn get_api_server_(api: String, custom: String) -> String {
|
||||
|
||||
#[inline]
|
||||
pub fn is_public(url: &str) -> bool {
|
||||
let url = url.to_ascii_lowercase();
|
||||
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
|
||||
}
|
||||
|
||||
@@ -1127,22 +1124,286 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String {
|
||||
format!("{}/api/audit/{}", url, typ)
|
||||
}
|
||||
|
||||
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
|
||||
/// Check if we should use raw TCP proxy for API calls.
|
||||
/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off,
|
||||
/// and the target URL belongs to the configured non-public API host.
|
||||
#[inline]
|
||||
fn should_use_raw_tcp_for_api(url: &str) -> bool {
|
||||
get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y"
|
||||
&& !use_ws()
|
||||
&& is_tcp_proxy_api_target(url)
|
||||
}
|
||||
|
||||
/// Check if we can attempt raw TCP proxy fallback for this target URL.
|
||||
#[inline]
|
||||
fn can_fallback_to_raw_tcp(url: &str) -> bool {
|
||||
!use_ws() && is_tcp_proxy_api_target(url)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool {
|
||||
if api_url.is_empty() || is_public(api_url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let target_host = url::Url::parse(url)
|
||||
.ok()
|
||||
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
|
||||
let api_host = url::Url::parse(api_url)
|
||||
.ok()
|
||||
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
|
||||
|
||||
matches!((target_host, api_host), (Some(target), Some(api)) if target == api)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_tcp_proxy_api_target(url: &str) -> bool {
|
||||
should_use_tcp_proxy_for_api_url(url, &ui_get_api_server())
|
||||
}
|
||||
|
||||
fn tcp_proxy_log_target(url: &str) -> String {
|
||||
url::Url::parse(url)
|
||||
.ok()
|
||||
.map(|parsed| {
|
||||
let mut redacted = format!("{}://", parsed.scheme());
|
||||
let Some(host) = parsed.host() else {
|
||||
return "<invalid-url>".to_owned();
|
||||
};
|
||||
redacted.push_str(&host.to_string());
|
||||
if let Some(port) = parsed.port() {
|
||||
redacted.push(':');
|
||||
redacted.push_str(&port.to_string());
|
||||
}
|
||||
redacted.push_str(parsed.path());
|
||||
redacted
|
||||
})
|
||||
.unwrap_or_else(|| "<invalid-url>".to_owned())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_tcp_proxy_addr() -> String {
|
||||
check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT)
|
||||
}
|
||||
|
||||
/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf.
|
||||
/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`,
|
||||
/// receives `HttpProxyResponse`.
|
||||
///
|
||||
/// The entire operation (connect + handshake + send + receive) is wrapped in
|
||||
/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at
|
||||
/// any stage cannot block the caller indefinitely.
|
||||
async fn tcp_proxy_request(
|
||||
method: &str,
|
||||
url: &str,
|
||||
body: &[u8],
|
||||
headers: Vec<HeaderEntry>,
|
||||
) -> ResultType<HttpProxyResponse> {
|
||||
let tcp_addr = get_tcp_proxy_addr();
|
||||
if tcp_addr.is_empty() {
|
||||
bail!("No rendezvous server configured for TCP proxy");
|
||||
}
|
||||
|
||||
let parsed = url::Url::parse(url)?;
|
||||
let path = if let Some(query) = parsed.query() {
|
||||
format!("{}?{}", parsed.path(), query)
|
||||
} else {
|
||||
parsed.path().to_string()
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"Sending {} {} via TCP proxy to {}",
|
||||
method,
|
||||
parsed.path(),
|
||||
tcp_addr
|
||||
);
|
||||
|
||||
let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT;
|
||||
timeout(overall_timeout, async {
|
||||
let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?;
|
||||
let key = crate::get_key(true).await;
|
||||
secure_tcp_silent(&mut conn, &key).await?;
|
||||
|
||||
let mut req = HttpProxyRequest::new();
|
||||
req.method = method.to_uppercase();
|
||||
req.path = path;
|
||||
req.headers = headers.into();
|
||||
req.body = Bytes::from(body.to_vec());
|
||||
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_http_proxy_request(req);
|
||||
conn.send(&msg_out).await?;
|
||||
|
||||
match conn.next().await {
|
||||
Some(Ok(bytes)) => {
|
||||
let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?;
|
||||
match msg_in.union {
|
||||
Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp),
|
||||
_ => bail!("Unexpected response from TCP proxy"),
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => bail!("TCP proxy read error: {}", e),
|
||||
None => bail!("TCP proxy connection closed without response"),
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Build HeaderEntry list from "Key: Value" style header string (used by post_request).
|
||||
/// If the caller supplies a Content-Type header it overrides the default `application/json`.
|
||||
fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
|
||||
let mut entries = Vec::new();
|
||||
let mut has_content_type = false;
|
||||
if !header.is_empty() {
|
||||
let tmp: Vec<&str> = header.splitn(2, ": ").collect();
|
||||
if tmp.len() == 2 {
|
||||
if tmp[0].eq_ignore_ascii_case("Content-Type") {
|
||||
has_content_type = true;
|
||||
}
|
||||
entries.push(HeaderEntry {
|
||||
name: tmp[0].into(),
|
||||
value: tmp[1].into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
if !has_content_type {
|
||||
entries.insert(
|
||||
0,
|
||||
HeaderEntry {
|
||||
name: "Content-Type".into(),
|
||||
value: "application/json".into(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
/// POST request via TCP proxy.
|
||||
async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType<String> {
|
||||
let headers = parse_simple_header(header);
|
||||
let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?;
|
||||
if !resp.error.is_empty() {
|
||||
bail!("TCP proxy error: {}", resp.error);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&resp.body).to_string())
|
||||
}
|
||||
|
||||
fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType<String> {
|
||||
if !resp.error.is_empty() {
|
||||
bail!("TCP proxy error: {}", resp.error);
|
||||
}
|
||||
|
||||
let mut response_headers = Map::new();
|
||||
for entry in resp.headers.iter() {
|
||||
response_headers.insert(entry.name.to_lowercase(), json!(entry.value));
|
||||
}
|
||||
|
||||
let mut result = Map::new();
|
||||
result.insert("status_code".to_string(), json!(resp.status));
|
||||
result.insert("headers".to_string(), Value::Object(response_headers));
|
||||
result.insert(
|
||||
"body".to_string(),
|
||||
json!(String::from_utf8_lossy(&resp.body)),
|
||||
);
|
||||
|
||||
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
|
||||
}
|
||||
|
||||
fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
|
||||
let v: Value = serde_json::from_str(header)?;
|
||||
if let Value::Object(obj) = v {
|
||||
Ok(obj
|
||||
.iter()
|
||||
.map(|(key, value)| HeaderEntry {
|
||||
name: key.clone(),
|
||||
value: value.as_str().unwrap_or_default().into(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect())
|
||||
} else {
|
||||
Err(anyhow!("HTTP header information parsing failed!"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback.
|
||||
async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> {
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(&url, &proxy_conf);
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let tls_type = get_cached_tls_type(tls_url);
|
||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let response = post_request_(
|
||||
&url,
|
||||
url,
|
||||
tls_url,
|
||||
body.clone(),
|
||||
body.to_owned(),
|
||||
header,
|
||||
tls_type,
|
||||
danger_accept_invalid_cert,
|
||||
danger_accept_invalid_cert,
|
||||
)
|
||||
.await?;
|
||||
Ok(response.text().await?)
|
||||
let status = response.status().as_u16();
|
||||
let text = response.text().await?;
|
||||
Ok((status, text))
|
||||
}
|
||||
|
||||
/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn`
|
||||
/// if the URL is eligible. 4xx responses are returned as-is.
|
||||
async fn with_tcp_proxy_fallback<HttpFut, TcpFut>(
|
||||
url: &str,
|
||||
method: &str,
|
||||
http_fn: HttpFut,
|
||||
tcp_fn: TcpFut,
|
||||
) -> ResultType<String>
|
||||
where
|
||||
HttpFut: Future<Output = ResultType<(u16, String)>>,
|
||||
TcpFut: Future<Output = ResultType<String>>,
|
||||
{
|
||||
if should_use_raw_tcp_for_api(url) {
|
||||
return tcp_fn.await;
|
||||
}
|
||||
|
||||
let http_result = http_fn.await;
|
||||
let should_fallback = match &http_result {
|
||||
Err(_) => true,
|
||||
Ok((status, _)) => *status >= 500,
|
||||
};
|
||||
|
||||
if should_fallback && can_fallback_to_raw_tcp(url) {
|
||||
log::warn!(
|
||||
"HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback",
|
||||
method,
|
||||
tcp_proxy_log_target(url),
|
||||
http_result
|
||||
.as_ref()
|
||||
.map(|(s, _)| *s)
|
||||
.map_err(|e| e.to_string()),
|
||||
);
|
||||
match tcp_fn.await {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(tcp_err) => {
|
||||
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http_result.map(|(_status, text)| text)
|
||||
}
|
||||
|
||||
/// POST request with raw TCP proxy support.
|
||||
/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy.
|
||||
/// - Otherwise tries HTTP first; on connection failure or 5xx status,
|
||||
/// falls back to TCP proxy if WS is off.
|
||||
/// - 4xx responses are returned as-is (server is reachable, business logic error).
|
||||
/// - If fallback also fails, returns the original HTTP result (text or error).
|
||||
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
|
||||
with_tcp_proxy_fallback(
|
||||
&url,
|
||||
"POST",
|
||||
post_request_http(&url, &body, header),
|
||||
post_request_via_tcp_proxy(&url, &body, header),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
@@ -1250,21 +1511,16 @@ async fn get_http_response_async(
|
||||
tls_type.unwrap_or(TlsType::Rustls),
|
||||
danger_accept_invalid_cert.unwrap_or(false),
|
||||
);
|
||||
let mut http_client = match method {
|
||||
let normalized_method = method.to_ascii_lowercase();
|
||||
let mut http_client = match normalized_method.as_str() {
|
||||
"get" => http_client.get(url),
|
||||
"post" => http_client.post(url),
|
||||
"put" => http_client.put(url),
|
||||
"delete" => http_client.delete(url),
|
||||
_ => return Err(anyhow!("The HTTP request method is not supported!")),
|
||||
};
|
||||
let v = serde_json::from_str(header)?;
|
||||
|
||||
if let Value::Object(obj) = v {
|
||||
for (key, value) in obj.iter() {
|
||||
http_client = http_client.header(key, value.as_str().unwrap_or_default());
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("HTTP header information parsing failed!"));
|
||||
for entry in parse_json_header_entries(header)? {
|
||||
http_client = http_client.header(entry.name, entry.value);
|
||||
}
|
||||
|
||||
if tls_type.is_some() && danger_accept_invalid_cert.is_some() {
|
||||
@@ -1344,6 +1600,51 @@ async fn get_http_response_async(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (status_code, json_string) so the caller can inspect the status
|
||||
/// without re-parsing the serialized JSON.
|
||||
async fn http_request_http(
|
||||
url: &str,
|
||||
method: &str,
|
||||
body: Option<String>,
|
||||
header: &str,
|
||||
) -> ResultType<(u16, String)> {
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||
let tls_type = get_cached_tls_type(tls_url);
|
||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let response = get_http_response_async(
|
||||
url,
|
||||
tls_url,
|
||||
method,
|
||||
body,
|
||||
header,
|
||||
tls_type,
|
||||
danger_accept_invalid_cert,
|
||||
danger_accept_invalid_cert,
|
||||
)
|
||||
.await?;
|
||||
// Serialize response headers
|
||||
let mut response_headers = Map::new();
|
||||
for (key, value) in response.headers() {
|
||||
response_headers.insert(key.to_string(), json!(value.to_str().unwrap_or("")));
|
||||
}
|
||||
|
||||
let status_code = response.status().as_u16();
|
||||
let response_body = response.text().await?;
|
||||
|
||||
// Construct the JSON object
|
||||
let mut result = Map::new();
|
||||
result.insert("status_code".to_string(), json!(status_code));
|
||||
result.insert("headers".to_string(), Value::Object(response_headers));
|
||||
result.insert("body".to_string(), json!(response_body));
|
||||
|
||||
// Convert map to JSON string
|
||||
let json_str = serde_json::to_string(&result)
|
||||
.map_err(|e| anyhow!("Failed to serialize response: {}", e))?;
|
||||
Ok((status_code, json_str))
|
||||
}
|
||||
|
||||
/// HTTP request with raw TCP proxy support.
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn http_request_sync(
|
||||
url: String,
|
||||
@@ -1351,44 +1652,28 @@ pub async fn http_request_sync(
|
||||
body: Option<String>,
|
||||
header: String,
|
||||
) -> ResultType<String> {
|
||||
let proxy_conf = Config::get_socks();
|
||||
let tls_url = get_url_for_tls(&url, &proxy_conf);
|
||||
let tls_type = get_cached_tls_type(tls_url);
|
||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||
let response = get_http_response_async(
|
||||
with_tcp_proxy_fallback(
|
||||
&url,
|
||||
tls_url,
|
||||
&method,
|
||||
body.clone(),
|
||||
&header,
|
||||
tls_type,
|
||||
danger_accept_invalid_cert,
|
||||
danger_accept_invalid_cert,
|
||||
http_request_http(&url, &method, body.clone(), &header),
|
||||
http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header),
|
||||
)
|
||||
.await?;
|
||||
// Serialize response headers
|
||||
let mut response_headers = serde_json::map::Map::new();
|
||||
for (key, value) in response.headers() {
|
||||
response_headers.insert(
|
||||
key.to_string(),
|
||||
serde_json::json!(value.to_str().unwrap_or("")),
|
||||
);
|
||||
}
|
||||
.await
|
||||
}
|
||||
|
||||
let status_code = response.status().as_u16();
|
||||
let response_body = response.text().await?;
|
||||
/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync).
|
||||
/// Returns a JSON string with status_code, headers, body (same format as http_request_sync).
|
||||
async fn http_request_via_tcp_proxy(
|
||||
url: &str,
|
||||
method: &str,
|
||||
body: Option<&str>,
|
||||
header: &str,
|
||||
) -> ResultType<String> {
|
||||
let headers = parse_json_header_entries(header)?;
|
||||
let body_bytes = body.unwrap_or("").as_bytes();
|
||||
|
||||
// Construct the JSON object
|
||||
let mut result = serde_json::map::Map::new();
|
||||
result.insert("status_code".to_string(), serde_json::json!(status_code));
|
||||
result.insert(
|
||||
"headers".to_string(),
|
||||
serde_json::Value::Object(response_headers),
|
||||
);
|
||||
result.insert("body".to_string(), serde_json::json!(response_body));
|
||||
|
||||
// Convert map to JSON string
|
||||
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
|
||||
let resp = tcp_proxy_request(method, url, body_bytes, headers).await?;
|
||||
http_proxy_response_to_json(resp)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -1651,7 +1936,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> {
|
||||
// Skip additional encryption when using WebSocket connections (wss://)
|
||||
// as WebSocket Secure (wss://) already provides transport layer encryption.
|
||||
// This doesn't affect the end-to-end encryption between clients,
|
||||
@@ -1684,7 +1969,9 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
});
|
||||
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
|
||||
conn.set_key(key);
|
||||
log::info!("Connection secured");
|
||||
if log_on_success {
|
||||
log::info!("Connection secured");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1695,6 +1982,14 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
secure_tcp_impl(conn, key, true).await
|
||||
}
|
||||
|
||||
async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
secure_tcp_impl(conn, key, false).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
|
||||
if pk.len() == 32 {
|
||||
@@ -1737,8 +2032,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 {
|
||||
@@ -2473,11 +2767,13 @@ mod tests {
|
||||
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://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("https://RustDesk.com"));
|
||||
assert!(is_public("http://www.rustdesk.com"));
|
||||
assert!(is_public("https://api.rustdesk.com"));
|
||||
|
||||
@@ -2490,6 +2786,193 @@ mod tests {
|
||||
assert!(!is_public("rustdesk.comhello.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_use_tcp_proxy_for_api_url() {
|
||||
assert!(should_use_tcp_proxy_for_api_url(
|
||||
"https://admin.example.com/api/login",
|
||||
"https://admin.example.com"
|
||||
));
|
||||
assert!(should_use_tcp_proxy_for_api_url(
|
||||
"https://admin.example.com:21114/api/login",
|
||||
"https://admin.example.com"
|
||||
));
|
||||
assert!(!should_use_tcp_proxy_for_api_url(
|
||||
"https://api.telegram.org/bot123/sendMessage",
|
||||
"https://admin.example.com"
|
||||
));
|
||||
assert!(!should_use_tcp_proxy_for_api_url(
|
||||
"https://admin.rustdesk.com/api/login",
|
||||
"https://admin.rustdesk.com"
|
||||
));
|
||||
assert!(!should_use_tcp_proxy_for_api_url(
|
||||
"https://admin.example.com/api/login",
|
||||
"not a url"
|
||||
));
|
||||
assert!(!should_use_tcp_proxy_for_api_url(
|
||||
"not a url",
|
||||
"https://admin.example.com"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() {
|
||||
struct RestoreCustomRendezvousServer(String);
|
||||
|
||||
impl Drop for RestoreCustomRendezvousServer {
|
||||
fn drop(&mut self) {
|
||||
Config::set_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
|
||||
self.0.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _restore = RestoreCustomRendezvousServer(Config::get_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER,
|
||||
));
|
||||
Config::set_option(
|
||||
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
|
||||
"1:2".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() {
|
||||
let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() {
|
||||
let err = http_request_via_tcp_proxy("not a url", "get", None, "[]")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("HTTP header information parsing failed!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_header_entries_preserves_single_content_type() {
|
||||
let headers = parse_json_header_entries(
|
||||
r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("text/plain")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_header_entries_does_not_add_default_content_type() {
|
||||
let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap();
|
||||
|
||||
assert!(!headers
|
||||
.iter()
|
||||
.any(|entry| entry.name.eq_ignore_ascii_case("Content-Type")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_header_respects_custom_content_type() {
|
||||
let headers = parse_simple_header("Content-Type: text/plain");
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("text/plain")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple_header_preserves_non_content_type_header() {
|
||||
let headers = parse_simple_header("Authorization: Bearer token");
|
||||
|
||||
assert!(headers.iter().any(|entry| {
|
||||
entry.name.eq_ignore_ascii_case("Authorization")
|
||||
&& entry.value.as_str() == "Bearer token"
|
||||
}));
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
|
||||
.map(|entry| entry.value.as_str()),
|
||||
Some("application/json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_proxy_log_target_redacts_query_only() {
|
||||
assert_eq!(
|
||||
tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"),
|
||||
"https://example.com/api/heartbeat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() {
|
||||
assert_eq!(
|
||||
tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"),
|
||||
"https://[2001:db8::1]:21114/api/heartbeat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_http_proxy_response_to_json() {
|
||||
let mut resp = HttpProxyResponse {
|
||||
status: 200,
|
||||
body: br#"{"ok":true}"#.to_vec().into(),
|
||||
..Default::default()
|
||||
};
|
||||
resp.headers.push(HeaderEntry {
|
||||
name: "Content-Type".into(),
|
||||
value: "application/json".into(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let json = http_proxy_response_to_json(resp).unwrap();
|
||||
let value: Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(value["status_code"], 200);
|
||||
assert_eq!(value["headers"]["content-type"], "application/json");
|
||||
assert_eq!(value["body"], r#"{"ok":true}"#);
|
||||
|
||||
let err = http_proxy_response_to_json(HttpProxyResponse {
|
||||
error: "dial failed".into(),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("TCP proxy error: dial failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mouse_event_constants_and_mask_layout() {
|
||||
use super::input::*;
|
||||
|
||||
@@ -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);
|
||||
@@ -187,7 +187,10 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
hbb_common::config::PeerConfig::preload_peers();
|
||||
{
|
||||
crate::platform::try_remove_temp_update_files();
|
||||
hbb_common::config::PeerConfig::preload_peers();
|
||||
}
|
||||
std::thread::spawn(move || crate::start_server(false, no_server));
|
||||
} else {
|
||||
#[cfg(windows)]
|
||||
@@ -202,17 +205,24 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
if config::is_disable_installation() {
|
||||
return None;
|
||||
}
|
||||
let res = platform::update_me(false);
|
||||
let text = match res {
|
||||
Ok(_) => translate("Update successfully!".to_string()),
|
||||
Err(err) => {
|
||||
log::error!("Failed with error: {err}");
|
||||
translate("Update failed!".to_string())
|
||||
|
||||
let text = match crate::platform::prepare_custom_client_update() {
|
||||
Err(e) => {
|
||||
log::error!("Error preparing custom client update: {}", e);
|
||||
"Update failed!".to_string()
|
||||
}
|
||||
Ok(false) => "Update failed!".to_string(),
|
||||
Ok(true) => match platform::update_me(false) {
|
||||
Ok(_) => "Updated successfully!".to_string(),
|
||||
Err(err) => {
|
||||
log::error!("Failed with error: {err}");
|
||||
"Update failed!".to_string()
|
||||
}
|
||||
},
|
||||
};
|
||||
Toast::new(Toast::POWERSHELL_APP_ID)
|
||||
.title(&config::APP_NAME.read().unwrap())
|
||||
.text1(&text)
|
||||
.text1(&translate(text))
|
||||
.sound(Some(Sound::Default))
|
||||
.duration(Duration::Short)
|
||||
.show()
|
||||
@@ -325,8 +335,8 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
log::info!("Starting update process...");
|
||||
let _text = match platform::update_me() {
|
||||
Ok(_) => {
|
||||
println!("{}", translate("Update successfully!".to_string()));
|
||||
log::info!("Update successfully!");
|
||||
println!("{}", translate("Updated successfully!".to_string()));
|
||||
log::info!("Updated successfully!");
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Update failed with error: {}", err);
|
||||
@@ -829,3 +839,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")
|
||||
}
|
||||
|
||||
@@ -1101,6 +1101,10 @@ pub fn main_get_api_server() -> String {
|
||||
get_api_server()
|
||||
}
|
||||
|
||||
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
|
||||
SyncReturn(resolve_avatar_url(avatar))
|
||||
}
|
||||
|
||||
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
|
||||
http_request(url, method, body, header)
|
||||
}
|
||||
@@ -1689,8 +1693,8 @@ pub fn main_get_temporary_password() -> String {
|
||||
ui_interface::temporary_password()
|
||||
}
|
||||
|
||||
pub fn main_get_permanent_password() -> String {
|
||||
ui_interface::permanent_password()
|
||||
pub fn main_set_permanent_password_with_result(password: String) -> bool {
|
||||
ui_interface::set_permanent_password_with_result(password)
|
||||
}
|
||||
|
||||
pub fn main_get_fingerprint() -> String {
|
||||
@@ -2068,10 +2072,6 @@ pub fn main_update_temporary_password() {
|
||||
update_temporary_password();
|
||||
}
|
||||
|
||||
pub fn main_set_permanent_password(password: String) {
|
||||
set_permanent_password(password);
|
||||
}
|
||||
|
||||
pub fn main_check_super_user_permission() -> bool {
|
||||
check_super_user_permission()
|
||||
}
|
||||
@@ -2419,16 +2419,23 @@ pub fn is_disable_installation() -> SyncReturn<bool> {
|
||||
}
|
||||
|
||||
pub fn is_preset_password() -> bool {
|
||||
config::HARD_SETTINGS
|
||||
let hard = config::HARD_SETTINGS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get("password")
|
||||
.map_or(false, |p| {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return p == &crate::ipc::get_permanent_password();
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return p == &config::Config::get_permanent_password();
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if hard.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// On desktop, service owns the authoritative config; query it via IPC and return only a boolean.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return crate::ipc::is_permanent_password_preset();
|
||||
|
||||
// On mobile, we have no service IPC; verify against local storage.
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return config::Config::matches_permanent_password_plain(&hard);
|
||||
}
|
||||
|
||||
// Don't call this function for desktop version.
|
||||
@@ -2759,6 +2766,15 @@ pub fn main_get_common(key: String) -> String {
|
||||
None => "",
|
||||
}
|
||||
.to_string();
|
||||
} else if key == "has-gnome-shortcuts-inhibitor-permission" {
|
||||
#[cfg(target_os = "linux")]
|
||||
return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string();
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return false.to_string();
|
||||
} else if key == "permanent-password-set" {
|
||||
return ui_interface::is_permanent_password_set().to_string();
|
||||
} else if key == "local-permanent-password-set" {
|
||||
return ui_interface::is_local_permanent_password_set().to_string();
|
||||
} else {
|
||||
if key.starts_with("download-data-") {
|
||||
let id = key.replace("download-data-", "");
|
||||
@@ -2771,10 +2787,13 @@ pub fn main_get_common(key: String) -> String {
|
||||
} else if key.starts_with("download-file-") {
|
||||
let _version = key.replace("download-file-", "");
|
||||
#[cfg(target_os = "windows")]
|
||||
return match crate::platform::windows::is_msi_installed() {
|
||||
Ok(true) => format!("rustdesk-{_version}-x86_64.msi"),
|
||||
Ok(false) => format!("rustdesk-{_version}-x86_64.exe"),
|
||||
Err(e) => {
|
||||
return match (
|
||||
crate::platform::windows::is_msi_installed(),
|
||||
crate::common::is_custom_client(),
|
||||
) {
|
||||
(Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"),
|
||||
(Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"),
|
||||
(Err(e), _) => {
|
||||
log::error!("Failed to check if is msi: {}", e);
|
||||
format!("error:update-failed-check-msi-tip")
|
||||
}
|
||||
@@ -2871,30 +2890,17 @@ pub fn main_set_common(_key: String, _value: String) {
|
||||
if let Some(f) = new_version_file.to_str() {
|
||||
// 1.4.0 does not support "--update"
|
||||
// But we can assume that the new version supports it.
|
||||
#[cfg(target_os = "windows")]
|
||||
if f.ends_with(".exe") {
|
||||
if let Err(e) =
|
||||
crate::platform::run_exe_in_cur_session(f, vec!["--update"], false)
|
||||
{
|
||||
log::error!("Failed to run the update exe: {}", e);
|
||||
}
|
||||
} else if f.ends_with(".msi") {
|
||||
if let Err(e) = crate::platform::update_me_msi(f, false) {
|
||||
log::error!("Failed to run the update msi: {}", e);
|
||||
}
|
||||
} else {
|
||||
// unreachable!()
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
match crate::platform::update_to(f) {
|
||||
Ok(_) => {
|
||||
log::info!("Update successfully!");
|
||||
log::info!("Update process is launched successfully!");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to update to new version, {}", e);
|
||||
fs::remove_file(f).ok();
|
||||
}
|
||||
}
|
||||
fs::remove_file(f).ok();
|
||||
}
|
||||
}
|
||||
} else if _key == "extract-update-dmg" {
|
||||
@@ -2920,6 +2926,29 @@ pub fn main_set_common(_key: String, _value: String) {
|
||||
} else if _key == "cancel-downloader" {
|
||||
crate::hbbs_http::downloader::cancel(&_value);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if _key == "clear-gnome-shortcuts-inhibitor-permission" {
|
||||
std::thread::spawn(move || {
|
||||
let (success, msg) =
|
||||
match crate::platform::linux::clear_gnome_shortcuts_inhibitor_permission() {
|
||||
Ok(_) => (true, "".to_owned()),
|
||||
Err(e) => (false, e.to_string()),
|
||||
};
|
||||
let data = HashMap::from([
|
||||
(
|
||||
"name",
|
||||
serde_json::json!("clear-gnome-shortcuts-inhibitor-permission-res"),
|
||||
),
|
||||
("success", serde_json::json!(success)),
|
||||
("msg", serde_json::json!(msg)),
|
||||
]);
|
||||
let _res = flutter::push_global_event(
|
||||
flutter::APP_TYPE_MAIN,
|
||||
serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_common_sync(
|
||||
@@ -3027,6 +3056,22 @@ pub mod server_side {
|
||||
return env.new_string(res).unwrap_or_default().into_raw();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn Java_ffi_FFI_getBuildinOption(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
key: JString,
|
||||
) -> jstring {
|
||||
let mut env = env;
|
||||
let res = if let Ok(key) = env.get_string(&key) {
|
||||
let key: String = key.into();
|
||||
super::get_builtin_option(&key)
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
return env.new_string(res).unwrap_or_default().into_raw();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled(
|
||||
env: JNIEnv,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::HbbHttpResponse;
|
||||
use crate::hbbs_http::create_http_client_with_url;
|
||||
use hbb_common::{config::LocalConfig, log, ResultType};
|
||||
use reqwest::blocking::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use serde_json::{Map, Value};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
@@ -17,6 +18,7 @@ lazy_static::lazy_static! {
|
||||
|
||||
const QUERY_INTERVAL_SECS: f32 = 1.0;
|
||||
const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
|
||||
|
||||
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
|
||||
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
|
||||
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";
|
||||
@@ -80,6 +82,10 @@ pub enum UserStatus {
|
||||
pub struct UserPayload {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub avatar: Option<String>,
|
||||
#[serde(default)]
|
||||
pub email: Option<String>,
|
||||
#[serde(default)]
|
||||
pub note: Option<String>,
|
||||
@@ -104,7 +110,7 @@ pub struct AuthBody {
|
||||
}
|
||||
|
||||
pub struct OidcSession {
|
||||
client: Option<Client>,
|
||||
warmed_api_server: Option<String>,
|
||||
state_msg: &'static str,
|
||||
failed_msg: String,
|
||||
code_url: Option<OidcAuthUrl>,
|
||||
@@ -131,7 +137,7 @@ impl Default for UserStatus {
|
||||
impl OidcSession {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
client: None,
|
||||
warmed_api_server: None,
|
||||
state_msg: REQUESTING_ACCOUNT_AUTH,
|
||||
failed_msg: "".to_owned(),
|
||||
code_url: None,
|
||||
@@ -144,11 +150,28 @@ impl OidcSession {
|
||||
|
||||
fn ensure_client(api_server: &str) {
|
||||
let mut write_guard = OIDC_SESSION.write().unwrap();
|
||||
if write_guard.client.is_none() {
|
||||
// This URL is used to detect the appropriate TLS implementation for the server.
|
||||
let login_option_url = format!("{}/api/login-options", &api_server);
|
||||
let client = create_http_client_with_url(&login_option_url);
|
||||
write_guard.client = Some(client);
|
||||
if write_guard.warmed_api_server.as_deref() == Some(api_server) {
|
||||
return;
|
||||
}
|
||||
// This URL is used to detect the appropriate TLS implementation for the server.
|
||||
let login_option_url = format!("{}/api/login-options", api_server);
|
||||
let _ = create_http_client_with_url(&login_option_url);
|
||||
write_guard.warmed_api_server = Some(api_server.to_owned());
|
||||
}
|
||||
|
||||
fn parse_hbb_http_response<T: DeserializeOwned>(body: &str) -> ResultType<HbbHttpResponse<T>> {
|
||||
let map = serde_json::from_str::<Map<String, Value>>(body)?;
|
||||
if let Some(error) = map.get("error") {
|
||||
if let Some(err) = error.as_str() {
|
||||
Ok(HbbHttpResponse::Error(err.to_owned()))
|
||||
} else {
|
||||
Ok(HbbHttpResponse::ErrorFormat)
|
||||
}
|
||||
} else {
|
||||
match serde_json::from_value(Value::Object(map)) {
|
||||
Ok(v) => Ok(HbbHttpResponse::Data(v)),
|
||||
Err(_) => Ok(HbbHttpResponse::DataTypeFormat),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,26 +182,15 @@ impl OidcSession {
|
||||
uuid: &str,
|
||||
) -> ResultType<HbbHttpResponse<OidcAuthUrl>> {
|
||||
Self::ensure_client(api_server);
|
||||
let resp = if let Some(client) = &OIDC_SESSION.read().unwrap().client {
|
||||
client
|
||||
.post(format!("{}/api/oidc/auth", api_server))
|
||||
.json(&serde_json::json!({
|
||||
"op": op,
|
||||
"id": id,
|
||||
"uuid": uuid,
|
||||
"deviceInfo": crate::ui_interface::get_login_device_info(),
|
||||
}))
|
||||
.send()?
|
||||
} else {
|
||||
hbb_common::bail!("http client not initialized");
|
||||
};
|
||||
let status = resp.status();
|
||||
match resp.try_into() {
|
||||
Ok(v) => Ok(v),
|
||||
Err(err) => {
|
||||
hbb_common::bail!("Http status: {}, err: {}", status, err);
|
||||
}
|
||||
}
|
||||
let body = serde_json::json!({
|
||||
"op": op,
|
||||
"id": id,
|
||||
"uuid": uuid,
|
||||
"deviceInfo": crate::ui_interface::get_login_device_info(),
|
||||
})
|
||||
.to_string();
|
||||
let resp = crate::post_request_sync(format!("{}/api/oidc/auth", api_server), body, "")?;
|
||||
Self::parse_hbb_http_response(&resp)
|
||||
}
|
||||
|
||||
fn query(
|
||||
@@ -192,11 +204,19 @@ impl OidcSession {
|
||||
&[("code", code), ("id", id), ("uuid", uuid)],
|
||||
)?;
|
||||
Self::ensure_client(api_server);
|
||||
if let Some(client) = &OIDC_SESSION.read().unwrap().client {
|
||||
Ok(client.get(url).send()?.try_into()?)
|
||||
} else {
|
||||
hbb_common::bail!("http client not initialized")
|
||||
#[derive(Deserialize)]
|
||||
struct HttpResponseBody {
|
||||
body: String,
|
||||
}
|
||||
|
||||
let resp = crate::http_request_sync(
|
||||
url.to_string(),
|
||||
"GET".to_owned(),
|
||||
None,
|
||||
"{}".to_owned(),
|
||||
)?;
|
||||
let resp = serde_json::from_str::<HttpResponseBody>(&resp)?;
|
||||
Self::parse_hbb_http_response(&resp.body)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
@@ -268,7 +288,13 @@ impl OidcSession {
|
||||
);
|
||||
LocalConfig::set_option(
|
||||
"user_info".to_owned(),
|
||||
serde_json::json!({ "name": auth_body.user.name, "status": auth_body.user.status }).to_string(),
|
||||
serde_json::json!({
|
||||
"name": auth_body.user.name,
|
||||
"display_name": auth_body.user.display_name,
|
||||
"avatar": auth_body.user.avatar,
|
||||
"status": auth_body.user.status
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,25 @@ pub fn download_file(
|
||||
auto_del_dur: Option<Duration>,
|
||||
) -> ResultType<String> {
|
||||
let id = url.clone();
|
||||
if DOWNLOADERS.lock().unwrap().contains_key(&id) {
|
||||
return Ok(id);
|
||||
// First pass: if a non-error downloader exists for this URL, reuse it.
|
||||
// If an errored downloader exists, remove it so this call can retry.
|
||||
let mut stale_path = None;
|
||||
{
|
||||
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
||||
if let Some(downloader) = downloaders.get(&id) {
|
||||
if downloader.error.is_none() {
|
||||
return Ok(id);
|
||||
}
|
||||
stale_path = downloader.path.clone();
|
||||
downloaders.remove(&id);
|
||||
}
|
||||
}
|
||||
if let Some(p) = stale_path {
|
||||
if p.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&p) {
|
||||
log::warn!("Failed to remove stale download file {}: {}", p.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = path.as_ref() {
|
||||
@@ -75,8 +92,26 @@ pub fn download_file(
|
||||
tx_cancel: tx,
|
||||
finished: false,
|
||||
};
|
||||
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
||||
downloaders.insert(id.clone(), downloader);
|
||||
// Second pass (atomic with insert) to avoid race with another concurrent caller.
|
||||
let mut stale_path_after_check = None;
|
||||
{
|
||||
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
||||
if let Some(existing) = downloaders.get(&id) {
|
||||
if existing.error.is_none() {
|
||||
return Ok(id);
|
||||
}
|
||||
stale_path_after_check = existing.path.clone();
|
||||
downloaders.remove(&id);
|
||||
}
|
||||
downloaders.insert(id.clone(), downloader);
|
||||
}
|
||||
if let Some(p) = stale_path_after_check {
|
||||
if p.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&p) {
|
||||
log::warn!("Failed to remove stale download file {}: {}", p.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id2 = id.clone();
|
||||
std::thread::spawn(
|
||||
|
||||
@@ -286,10 +286,14 @@ fn heartbeat_url() -> String {
|
||||
|
||||
fn handle_config_options(config_options: HashMap<String, String>) {
|
||||
let mut options = Config::get_options();
|
||||
let default_settings = config::DEFAULT_SETTINGS.read().unwrap().clone();
|
||||
config_options
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
if v.is_empty() {
|
||||
// Priority: user config > default advanced options.
|
||||
// Only when default advanced options are also empty, remove user option (fallback to built-in default);
|
||||
// otherwise insert an empty value so user config remains present.
|
||||
if v.is_empty() && default_settings.get(k).map_or("", |v| v).is_empty() {
|
||||
options.remove(k);
|
||||
} else {
|
||||
options.insert(k.to_string(), v.to_string());
|
||||
|
||||
138
src/ipc.rs
138
src/ipc.rs
@@ -226,6 +226,7 @@ pub enum Data {
|
||||
is_terminal: bool,
|
||||
peer_id: String,
|
||||
name: String,
|
||||
avatar: String,
|
||||
authorized: bool,
|
||||
port_forward: String,
|
||||
keyboard: bool,
|
||||
@@ -631,8 +632,29 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
value = Some(Config::get_id());
|
||||
} else if name == "temporary-password" {
|
||||
value = Some(password::temporary_password());
|
||||
} else if name == "permanent-password" {
|
||||
value = Some(Config::get_permanent_password());
|
||||
} else if name == "permanent-password-storage-and-salt" {
|
||||
let (storage, salt) = Config::get_local_permanent_password_storage_and_salt();
|
||||
value = Some(storage + "\n" + &salt);
|
||||
} else if name == "permanent-password-set" {
|
||||
value = Some(if Config::has_permanent_password() {
|
||||
"Y".to_owned()
|
||||
} else {
|
||||
"N".to_owned()
|
||||
});
|
||||
} else if name == "permanent-password-is-preset" {
|
||||
let hard = config::HARD_SETTINGS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get("password")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let is_preset =
|
||||
!hard.is_empty() && Config::matches_permanent_password_plain(&hard);
|
||||
value = Some(if is_preset {
|
||||
"Y".to_owned()
|
||||
} else {
|
||||
"N".to_owned()
|
||||
});
|
||||
} else if name == "salt" {
|
||||
value = Some(Config::get_salt());
|
||||
} else if name == "rendezvous_server" {
|
||||
@@ -668,13 +690,24 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
allow_err!(stream.send(&Data::Config((name, value))).await);
|
||||
}
|
||||
Some(value) => {
|
||||
let mut updated = true;
|
||||
if name == "id" {
|
||||
Config::set_key_confirmed(false);
|
||||
Config::set_id(&value);
|
||||
} else if name == "temporary-password" {
|
||||
password::update_temporary_password();
|
||||
} else if name == "permanent-password" {
|
||||
Config::set_permanent_password(&value);
|
||||
if Config::is_disable_change_permanent_password() {
|
||||
log::warn!("Changing permanent password is disabled");
|
||||
updated = false;
|
||||
} else {
|
||||
Config::set_permanent_password(&value);
|
||||
}
|
||||
// Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to
|
||||
// distinguish "accepted by daemon" vs "IPC send succeeded" without
|
||||
// reading back any secret.
|
||||
let ack = if updated { "Y" } else { "N" }.to_owned();
|
||||
allow_err!(stream.send(&Data::Config((name.clone(), Some(ack)))).await);
|
||||
} else if name == "salt" {
|
||||
Config::set_salt(&value);
|
||||
} else if name == "voice-call-input" {
|
||||
@@ -684,7 +717,9 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
log::info!("{} updated", name);
|
||||
if updated {
|
||||
log::info!("{} updated", name);
|
||||
}
|
||||
}
|
||||
},
|
||||
Data::Options(value) => match value {
|
||||
@@ -1142,13 +1177,57 @@ pub fn update_temporary_password() -> ResultType<()> {
|
||||
set_config("temporary-password", "".to_owned())
|
||||
}
|
||||
|
||||
pub fn get_permanent_password() -> String {
|
||||
if let Ok(Some(v)) = get_config("permanent-password") {
|
||||
Config::set_permanent_password(&v);
|
||||
v
|
||||
} else {
|
||||
Config::get_permanent_password()
|
||||
fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> ResultType<()> {
|
||||
let Some(payload) = payload else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some((storage, salt)) = payload.split_once('\n') else {
|
||||
bail!("Invalid permanent-password-storage-and-salt payload");
|
||||
};
|
||||
|
||||
if storage.is_empty() {
|
||||
Config::set_permanent_password_storage_for_sync("", "")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Config::set_permanent_password_storage_for_sync(storage, salt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sync_permanent_password_storage_from_daemon() -> ResultType<()> {
|
||||
let v = get_config("permanent-password-storage-and-salt")?;
|
||||
apply_permanent_password_storage_and_salt_payload(v.as_deref())
|
||||
}
|
||||
|
||||
async fn sync_permanent_password_storage_from_daemon_async() -> ResultType<()> {
|
||||
let ms_timeout = 1_000;
|
||||
let v = get_config_async("permanent-password-storage-and-salt", ms_timeout).await?;
|
||||
apply_permanent_password_storage_and_salt_payload(v.as_deref())
|
||||
}
|
||||
|
||||
pub fn is_permanent_password_set() -> bool {
|
||||
match get_config("permanent-password-set") {
|
||||
Ok(Some(v)) => {
|
||||
let v = v.trim();
|
||||
return v == "Y";
|
||||
}
|
||||
Ok(None) => {
|
||||
// No response/value (timeout).
|
||||
}
|
||||
Err(_) => {
|
||||
// Connection error.
|
||||
}
|
||||
}
|
||||
log::warn!("Failed to query permanent password state from daemon");
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_permanent_password_preset() -> bool {
|
||||
if let Ok(Some(v)) = get_config("permanent-password-is-preset") {
|
||||
let v = v.trim();
|
||||
return v == "Y";
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_fingerprint() -> String {
|
||||
@@ -1158,8 +1237,41 @@ pub fn get_fingerprint() -> String {
|
||||
}
|
||||
|
||||
pub fn set_permanent_password(v: String) -> ResultType<()> {
|
||||
Config::set_permanent_password(&v);
|
||||
set_config("permanent-password", v)
|
||||
if Config::is_disable_change_permanent_password() {
|
||||
bail!("Changing permanent password is disabled");
|
||||
}
|
||||
if set_permanent_password_with_ack(v)? {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Changing permanent password was rejected by daemon");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn set_permanent_password_with_ack(v: String) -> ResultType<bool> {
|
||||
set_permanent_password_with_ack_async(v).await
|
||||
}
|
||||
|
||||
async fn set_permanent_password_with_ack_async(v: String) -> ResultType<bool> {
|
||||
// The daemon ACK/NACK is expected quickly since it applies the config in-process.
|
||||
let ms_timeout = 1_000;
|
||||
let mut c = connect(ms_timeout, "").await?;
|
||||
c.send_config("permanent-password", v).await?;
|
||||
if let Some(Data::Config((name2, Some(v)))) = c.next_timeout(ms_timeout).await? {
|
||||
if name2 == "permanent-password" {
|
||||
let v = v.trim();
|
||||
let ok = v == "Y";
|
||||
if ok {
|
||||
// Ensure the hashed permanent password storage is written to the user config file.
|
||||
// This sync must not affect the daemon ACK outcome.
|
||||
if let Err(err) = sync_permanent_password_storage_from_daemon_async().await {
|
||||
log::warn!("Failed to sync permanent password storage from daemon: {err}");
|
||||
}
|
||||
}
|
||||
return Ok(ok);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
@@ -1583,6 +1695,6 @@ mod test {
|
||||
#[test]
|
||||
fn verify_ffi_enum_data_size() {
|
||||
println!("{}", std::mem::size_of::<Data>());
|
||||
assert!(std::mem::size_of::<Data>() <= 96);
|
||||
assert!(std::mem::size_of::<Data>() <= 120);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "اعدادات لوحة المفاتيح"),
|
||||
("Full Access", "وصول كامل"),
|
||||
("Screen Share", "مشاركة الشاشة"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."),
|
||||
("ubuntu-21-04-required", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."),
|
||||
("wayland-requires-higher-linux-version", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."),
|
||||
("xdp-portal-unavailable", "لاقط شاشة Wayland فشل. بوابة سطح مكتب XDG ربما توقفت عن العمل او حدث خطأ بها. جرب اعادة تشغليها عن طريق 'systemctl --user restart xdg-desktop-portal'."),
|
||||
("JumpLink", "رابط القفز"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "الرجاء اختيار شاشة لمشاركتها (تعمل على جانب القرين)."),
|
||||
("Show RustDesk", "عرض RustDesk"),
|
||||
("This PC", "هذا الحاسب"),
|
||||
("or", "او"),
|
||||
("Continue with", "متابعة مع"),
|
||||
("Elevate", "ارتقاء"),
|
||||
("Zoom cursor", "تكبير المؤشر"),
|
||||
("Accept sessions via password", "قبول الجلسات عبر كلمة المرور"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}", "متابعة مع {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Налады клавіятуры"),
|
||||
("Full Access", "Поўны доступ"),
|
||||
("Screen Share", "Дэманстрацыя экрана"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland патрабуе Ubuntu версіі 21.04 або навейшай."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыву Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."),
|
||||
("ubuntu-21-04-required", "Wayland патрабуе Ubuntu версіі 21.04 або навейшай."),
|
||||
("wayland-requires-higher-linux-version", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыву Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Перайсці па спасылцы"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Выберыце экран для дэманстрацыі (кіруецца аддаленай стараной)."),
|
||||
("Show RustDesk", "Паказаць RustDesk"),
|
||||
("This PC", "Гэты кампутар"),
|
||||
("or", "або"),
|
||||
("Continue with", "Працягнуць з"),
|
||||
("Elevate", "Павысіць"),
|
||||
("Zoom cursor", "Павялічэнне курсора"),
|
||||
("Accept sessions via password", "Прымаць сеансы па паролю"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}", "Працягнуць з {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Настройки на клавиатурата"),
|
||||
("Full Access", "Пълен достъп"),
|
||||
("Screen Share", "Споделяне на екрана"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland изисква Ubuntu 21.04 или по-нов"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."),
|
||||
("ubuntu-21-04-required", "Wayland изисква Ubuntu 21.04 или по-нов"),
|
||||
("wayland-requires-higher-linux-version", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Препратка"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."),
|
||||
("Show RustDesk", "Покажи RustDesk"),
|
||||
("This PC", "Този компютър"),
|
||||
("or", "или"),
|
||||
("Continue with", "Продължи с"),
|
||||
("Elevate", "Повишаване"),
|
||||
("Zoom cursor", "Уголемяване курсор"),
|
||||
("Accept sessions via password", "Приемане сесии чрез парола"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}", "Продължи с {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Configuració del teclat"),
|
||||
("Full Access", "Accés complet"),
|
||||
("Screen Share", "Compartició de pantalla"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o superior"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."),
|
||||
("ubuntu-21-04-required", "Wayland requereix Ubuntu 21.04 o superior"),
|
||||
("wayland-requires-higher-linux-version", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Marcador"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"),
|
||||
("Show RustDesk", "Mostra el RustDesk"),
|
||||
("This PC", "Aquest equip"),
|
||||
("or", "o"),
|
||||
("Continue with", "Continua amb"),
|
||||
("Elevate", "Permisos ampliats"),
|
||||
("Zoom cursor", "Escala del ratolí"),
|
||||
("Accept sessions via password", "Accepta les sessions mitjançant una contrasenya"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "键盘设置"),
|
||||
("Full Access", "完全访问"),
|
||||
("Screen Share", "仅共享屏幕"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"),
|
||||
("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
|
||||
("wayland-requires-higher-linux-version", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "查看"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"),
|
||||
("Show RustDesk", "显示 RustDesk"),
|
||||
("This PC", "此电脑"),
|
||||
("or", "或"),
|
||||
("Continue with", "使用"),
|
||||
("Elevate", "提权"),
|
||||
("Zoom cursor", "缩放光标"),
|
||||
("Accept sessions via password", "只允许密码访问"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}", "使用 {} 登录"),
|
||||
("Display Name", "显示名称"),
|
||||
("password-hidden-tip", "永久密码已设置(已隐藏)"),
|
||||
("preset-password-in-use-tip", "当前使用预设密码"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Nastavení klávesnice"),
|
||||
("Full Access", "Úplný přístup"),
|
||||
("Screen Share", "Sdílení obrazovky"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."),
|
||||
("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."),
|
||||
("wayland-requires-higher-linux-version", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "JumpLink"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protistrany)."),
|
||||
("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"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Tastaturindstillinger"),
|
||||
("Full Access", "Fuld adgang"),
|
||||
("Screen Share", "Skærmdeling"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu version 21.04 eller nyere."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."),
|
||||
("ubuntu-21-04-required", "Wayland kræver Ubuntu version 21.04 eller nyere."),
|
||||
("wayland-requires-higher-linux-version", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "JumpLink"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på modtagersiden)."),
|
||||
("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"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Tastatureinstellungen"),
|
||||
("Full Access", "Vollzugriff"),
|
||||
("Screen Share", "Bildschirmfreigabe"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."),
|
||||
("JumpLink", "View"),
|
||||
("ubuntu-21-04-required", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."),
|
||||
("wayland-requires-higher-linux-version", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."),
|
||||
("xdp-portal-unavailable", "Die Bildschirmaufnahme mit Wayland ist fehlgeschlagen. Das XDG-Desktop-Portal ist möglicherweise abgestürzt oder nicht verfügbar. Versuchen Sie, es mit `systemctl --user restart xdg-desktop-portal` neu zu starten."),
|
||||
("JumpLink", "Anzeigen"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."),
|
||||
("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 +562,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"),
|
||||
@@ -730,11 +730,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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", ""),
|
||||
("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", ""),
|
||||
("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 {}"),
|
||||
("Display Name", "Anzeigename"),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
443
src/lang/el.rs
443
src/lang/el.rs
@@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "Κατάσταση"),
|
||||
("Your Desktop", "Ο σταθμός εργασίας σας"),
|
||||
("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."),
|
||||
("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το ID και τον κωδικό πρόσβασης."),
|
||||
("Password", "Κωδικός πρόσβασης"),
|
||||
("Ready", "Έτοιμο"),
|
||||
("Established", "Συνδέθηκε"),
|
||||
@@ -19,16 +19,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recent sessions", "Πρόσφατες συνεδρίες"),
|
||||
("Address book", "Βιβλίο διευθύνσεων"),
|
||||
("Confirmation", "Επιβεβαίωση"),
|
||||
("TCP tunneling", "TCP tunneling"),
|
||||
("TCP tunneling", "Σήραγγα TCP"),
|
||||
("Remove", "Κατάργηση"),
|
||||
("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"),
|
||||
("Refresh random password", "Ανανέωση τυχαίου κωδικού πρόσβασης"),
|
||||
("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"),
|
||||
("Enable keyboard/mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"),
|
||||
("Enable clipboard", "Ενεργοποίηση προχείρου"),
|
||||
("Enable file transfer", "Ενεργοποίηση μεταφοράς αρχείων"),
|
||||
("Enable TCP tunneling", "Ενεργοποίηση TCP tunneling"),
|
||||
("Enable TCP tunneling", "Ενεργοποίηση σήραγγας TCP"),
|
||||
("IP Whitelisting", "Λίστα επιτρεπόμενων IP"),
|
||||
("ID/Relay Server", "Διακομιστής ID/Αναμετάδοσης"),
|
||||
("ID/Relay Server", "ID/Διακομιστής Αναμετάδοσης"),
|
||||
("Import server config", "Εισαγωγή διαμόρφωσης διακομιστή"),
|
||||
("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"),
|
||||
("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"),
|
||||
@@ -36,14 +36,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"),
|
||||
("Clipboard is empty", "Το πρόχειρο είναι κενό"),
|
||||
("Stop service", "Διακοπή υπηρεσίας"),
|
||||
("Change ID", "Αλλαγή αναγνωριστικού ID"),
|
||||
("Change ID", "Αλλαγή του ID σας"),
|
||||
("Your new ID", "Το νέο σας ID"),
|
||||
("length %min% to %max%", "μέγεθος από %min% έως %max%"),
|
||||
("starts with a letter", "ξεκινά με γράμμα"),
|
||||
("allowed characters", "επιτρεπόμενοι χαρακτήρες"),
|
||||
("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9, - (παύλα) και _ (κάτω παύλα). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."),
|
||||
("Website", "Ιστότοπος"),
|
||||
("About", "Πληροφορίες"),
|
||||
("About", "Σχετικά"),
|
||||
("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"),
|
||||
("Privacy Statement", "Πολιτική απορρήτου"),
|
||||
("Mute", "Σίγαση"),
|
||||
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Audio Input", "Είσοδος ήχου"),
|
||||
("Enhancements", "Βελτιώσεις"),
|
||||
("Hardware Codec", "Κωδικοποιητής υλικού"),
|
||||
("Adaptive bitrate", "Adaptive bitrate"),
|
||||
("Adaptive bitrate", "Προσαρμοστικός ρυθμός μετάδοσης bit"),
|
||||
("ID Server", "Διακομιστής ID"),
|
||||
("Relay Server", "Διακομιστής αναμετάδοσης"),
|
||||
("API Server", "Διακομιστής API"),
|
||||
@@ -67,18 +67,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Skip", "Παράλειψη"),
|
||||
("Close", "Κλείσιμο"),
|
||||
("Retry", "Δοκίμασε ξανά"),
|
||||
("OK", "ΟΚ"),
|
||||
("OK", "Εντάξει"),
|
||||
("Password Required", "Απαιτείται κωδικός πρόσβασης"),
|
||||
("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"),
|
||||
("Remember password", "Απομνημόνευση κωδικού πρόσβασης"),
|
||||
("Wrong Password", "Λάθος κωδικός πρόσβασης"),
|
||||
("Do you want to enter again?", "Επανασύνδεση;"),
|
||||
("Do you want to enter again?", "Θέλετε να γίνει επανασύνδεση;"),
|
||||
("Connection Error", "Σφάλμα σύνδεσης"),
|
||||
("Error", "Σφάλμα"),
|
||||
("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"),
|
||||
("Connecting...", "Σύνδεση..."),
|
||||
("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."),
|
||||
("Please try 1 minute later", "Παρακαλώ ξαναδοκιμάστε σε 1 λεπτό"),
|
||||
("Please try 1 minute later", "Παρακαλώ δοκιμάστε ξανά σε 1 λεπτό"),
|
||||
("Login Error", "Σφάλμα εισόδου"),
|
||||
("Successful", "Επιτυχής"),
|
||||
("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."),
|
||||
@@ -101,10 +101,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Select All", "Επιλογή όλων"),
|
||||
("Unselect All", "Κατάργηση επιλογής όλων"),
|
||||
("Empty Directory", "Κενός φάκελος"),
|
||||
("Not an empty directory", "Ο φάκελος δεν είναι κενός"),
|
||||
("Not an empty directory", "Η διαδρομή δεν είναι κενή"),
|
||||
("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"),
|
||||
("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κενό φάκελο;"),
|
||||
("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτού του φακέλου;"),
|
||||
("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την κενή διαδρομή;"),
|
||||
("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτής της διαδρομής;"),
|
||||
("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"),
|
||||
("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"),
|
||||
("Deleting", "Διαγραφή"),
|
||||
@@ -133,8 +133,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"),
|
||||
("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"),
|
||||
("Refresh", "Ανανέωση"),
|
||||
("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"),
|
||||
("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"),
|
||||
("ID does not exist", "Το ID αυτό δεν υπάρχει"),
|
||||
("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με τον διακομιστή"),
|
||||
("Please try later", "Παρακαλώ δοκιμάστε αργότερα"),
|
||||
("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"),
|
||||
("Key mismatch", "Μη έγκυρο κλειδί"),
|
||||
@@ -146,17 +146,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Set Password", "Ορίστε κωδικό πρόσβασης"),
|
||||
("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"),
|
||||
("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"),
|
||||
("Click to upgrade", "Αναβάθμιση τώρα"),
|
||||
("Click to upgrade", "Κάντε κλίκ για αναβάθμιση τώρα"),
|
||||
("Configure", "Διαμόρφωση"),
|
||||
("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."),
|
||||
("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."),
|
||||
("config_acc", "Για να ελέγξετε την επιφάνεια εργασίας σας από απόσταση, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Προσβασιμότητας\"."),
|
||||
("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στην επιφάνεια εργασίας σας, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή οθόνης\"."),
|
||||
("Installing ...", "Γίνεται εγκατάσταση ..."),
|
||||
("Install", "Εγκατάσταση"),
|
||||
("Installation", "Η εγκατάσταση"),
|
||||
("Installation Path", "Διαδρομή εγκατάστασης"),
|
||||
("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"),
|
||||
("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"),
|
||||
("agreement_tip", "Με την εγκατάσταση αποδέχεστε την άδεια χρήσης"),
|
||||
("agreement_tip", "Με την εγκατάσταση, αποδέχεστε την άδεια χρήσης"),
|
||||
("Accept and Install", "Αποδοχή και εγκατάσταση"),
|
||||
("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"),
|
||||
("Generating ...", "Δημιουργία ..."),
|
||||
@@ -170,8 +170,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Local Port", "Τοπική θύρα"),
|
||||
("Local Address", "Τοπική διεύθυνση"),
|
||||
("Change Local Port", "Αλλαγή τοπικής θύρας"),
|
||||
("setup_server_tip", "Για πιο γρήγορη σύνδεση, ρυθμίστε τον δικό σας διακομιστή σύνδεσης"),
|
||||
("Too short, at least 6 characters.", "Πολύ μικρό, τουλάχιστον 6 χαρακτήρες."),
|
||||
("setup_server_tip", "Για πιο γρήγορη σύνδεση, παρακαλούμε να ρυθμίστε τον δικό σας διακομιστή σύνδεσης"),
|
||||
("Too short, at least 6 characters.", "Πολύ μικρό, χρειάζεται τουλάχιστον 6 χαρακτήρες."),
|
||||
("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."),
|
||||
("Permissions", "Άδειες"),
|
||||
("Accept", "Αποδοχή"),
|
||||
@@ -183,7 +183,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"),
|
||||
("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"),
|
||||
("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"),
|
||||
("Enter Remote ID", "Εισαγωγή απομακρυσμένου ID"),
|
||||
("Enter Remote ID", "Εισαγωγή του απομακρυσμένου ID"),
|
||||
("Enter your password", "Εισάγετε τον κωδικό σας"),
|
||||
("Logging in...", "Γίνεται σύνδεση..."),
|
||||
("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"),
|
||||
@@ -200,35 +200,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"),
|
||||
("Reboot required", "Απαιτείται επανεκκίνηση"),
|
||||
("Unsupported display server", "Μη υποστηριζόμενος διακομιστής εμφάνισης "),
|
||||
("x11 expected", "απαιτείται X11"),
|
||||
("x11 expected", "αναμένεται X11"),
|
||||
("Port", "Θύρα"),
|
||||
("Settings", "Ρυθμίσεις"),
|
||||
("Username", "Όνομα χρήστη"),
|
||||
("Invalid port", "Μη έγκυρη θύρα"),
|
||||
("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"),
|
||||
("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"),
|
||||
("Closed manually by the peer", "Τερματίστηκε από τον απομακρυσμένο σταθμό"),
|
||||
("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης διαμόρφωσης"),
|
||||
("Run without install", "Εκτέλεση χωρίς εγκατάσταση"),
|
||||
("Connect via relay", "Πραγματοποίηση σύνδεση μέσω αναμεταδότη"),
|
||||
("Always connect via relay", "Σύνδεση πάντα μέσω αναμεταδότη"),
|
||||
("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"),
|
||||
("Connect via relay", "Σύνδεση μέσω αναμεταδότη"),
|
||||
("Always connect via relay", "Να γίνεται σύνδεση πάντα μέσω αναμεταδότη"),
|
||||
("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων να έχουν πρόσβαση σε εμένα"),
|
||||
("Login", "Σύνδεση"),
|
||||
("Verify", "Επαλήθευση"),
|
||||
("Remember me", "Να με θυμάσαι"),
|
||||
("Trust this device", "Εμπιστεύομαι αυτή την συσκευή"),
|
||||
("Trust this device", "Να εμπιστεύομαι αυτή την συσκευή"),
|
||||
("Verification code", "Κωδικός επαλήθευσης"),
|
||||
("verification_tip", "Εντοπίστηκε νέα συσκευή και εστάλη ένας κωδικός επαλήθευσης στην καταχωρισμένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνδεθείτε ξανά."),
|
||||
("verification_tip", "Ένας κωδικός επαλήθευσης έχει σταλεί στην καταχωρημένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνεχίσετε τη σύνδεση."),
|
||||
("Logout", "Αποσύνδεση"),
|
||||
("Tags", "Ετικέτες"),
|
||||
("Search ID", "Αναζήτηση ID"),
|
||||
("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"),
|
||||
("Add ID", "Προσθήκη αναγνωριστικού ID"),
|
||||
("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, κενό ή νέα γραμμή"),
|
||||
("Add ID", "Προσθήκη ID"),
|
||||
("Add Tag", "Προσθήκη ετικέτας"),
|
||||
("Unselect all tags", "Κατάργηση επιλογής όλων των ετικετών"),
|
||||
("Unselect all tags", "Αποεπιλογή όλων των ετικετών"),
|
||||
("Network error", "Σφάλμα δικτύου"),
|
||||
("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"),
|
||||
("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"),
|
||||
("Wrong credentials", "Λάθος διαπιστευτήρια"),
|
||||
("The verification code is incorrect or has expired", ""),
|
||||
("The verification code is incorrect or has expired", "Ο κωδικός επαλήθευσης είναι λανθασμένος ή έχει λήξει"),
|
||||
("Edit Tag", "Επεξεργασία ετικέτας"),
|
||||
("Forget Password", "Διαγραφή απομνημονευμένου κωδικού"),
|
||||
("Favorites", "Αγαπημένα"),
|
||||
@@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Socks5 Proxy", "Διαμεσολαβητής Socks5"),
|
||||
("Socks5/Http(s) Proxy", "Διαμεσολαβητής Socks5/Http(s)"),
|
||||
("Discovered", "Ανακαλύφθηκαν"),
|
||||
("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"),
|
||||
("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος."),
|
||||
("Remote ID", "Απομακρυσμένο ID"),
|
||||
("Paste", "Επικόλληση"),
|
||||
("Paste here?", "Επικόλληση εδώ;"),
|
||||
@@ -262,28 +262,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Pinch to Zoom", "Τσίμπημα για ζουμ"),
|
||||
("Canvas Zoom", "Ζουμ σε καμβά"),
|
||||
("Reset canvas", "Επαναφορά καμβά"),
|
||||
("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"),
|
||||
("No permission of file transfer", "Δεν υπάρχει άδεια για την μεταφορά αρχείων"),
|
||||
("Note", "Σημείωση"),
|
||||
("Connection", "Σύνδεση"),
|
||||
("Share screen", "Κοινή χρήση οθόνης"),
|
||||
("Chat", "Κουβέντα"),
|
||||
("Total", "Σύνολο"),
|
||||
("items", "στοιχεία"),
|
||||
("Selected", "Επιλεγμένο"),
|
||||
("Screen Capture", "Αποτύπωση οθόνης"),
|
||||
("Selected", "Επιλεγμένα"),
|
||||
("Screen Capture", "Καταγραφή οθόνης"),
|
||||
("Input Control", "Έλεγχος εισόδου"),
|
||||
("Audio Capture", "Εγγραφή ήχου"),
|
||||
("Do you accept?", "Δέχεσαι;"),
|
||||
("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"),
|
||||
("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"),
|
||||
("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισόδου για Android;"),
|
||||
("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."),
|
||||
("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."),
|
||||
("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."),
|
||||
("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."),
|
||||
("android_stop_service_tip", "Η απενεργοποίηση της υπηρεσίας θα αποσυνδέσει αυτόματα όλες τις εγκατεστημένες συνδέσεις."),
|
||||
("android_version_audio_tip", "Η έκδοση Android που διαθέτετε δεν υποστηρίζει εγγραφή ήχου, ενημερώστε το σε Android 10 ή νεότερη έκδοση, εάν είναι δυνατόν."),
|
||||
("android_start_service_tip", ""),
|
||||
("android_permission_may_not_change_tip", ""),
|
||||
("android_input_permission_tip2", "Παρακαλούμε να μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."),
|
||||
("android_new_connection_tip", "Έχει ληφθεί νέο αίτημα ελέγχου, το οποίο θέλει να ελέγξει την τρέχουσα συσκευή σας."),
|
||||
("android_service_will_start_tip", "Η ενεργοποίηση της \"Καταγραφής οθόνης\" θα ξεκινήσει αυτόματα την υπηρεσία, επιτρέποντας σε άλλες συσκευές να ζητήσουν σύνδεση με τη συσκευή σας."),
|
||||
("android_stop_service_tip", "Το κλείσιμο της υπηρεσίας αυτής θα κλείσει αυτόματα όλες τις υπάρχουσες συνδέσεις."),
|
||||
("android_version_audio_tip", "Η τρέχουσα έκδοση Android δεν υποστηρίζει εγγραφή ήχου, αναβαθμίστε σε Android 10 ή νεότερη έκδοση."),
|
||||
("android_start_service_tip", "Πατήστε [Έναρξη υπηρεσίας] ή ενεργοποιήστε την άδεια [Καταγραφή οθόνης] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."),
|
||||
("android_permission_may_not_change_tip", "Τα δικαιώματα για τις καθιερωμένες συνδέσεις δεν μπορούν να αλλάξουν άμεσα μέχρι να επανασυνδεθούν."),
|
||||
("Account", "Λογαριασμός"),
|
||||
("Overwrite", "Αντικατάσταση"),
|
||||
("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"),
|
||||
@@ -293,14 +293,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Succeeded", "Επιτυχής"),
|
||||
("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"),
|
||||
("Unsupported", "Δεν υποστηρίζεται"),
|
||||
("Peer denied", "Ο απομακρυσμένος σταθμός απέρριψε τη σύνδεση"),
|
||||
("Peer denied", "Ο απομακρυσμένος σταθμός έχει απορριφθεί"),
|
||||
("Please install plugins", "Παρακαλώ εγκαταστήστε τα πρόσθετα"),
|
||||
("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"),
|
||||
("Failed to turn off", "Αποτυχία απενεργοποίησης"),
|
||||
("Turned off", "Απενεργοποιημένο"),
|
||||
("Language", "Γλώσσα"),
|
||||
("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"),
|
||||
("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"),
|
||||
("Keep RustDesk background service", "Διατήρηση της υπηρεσίας παρασκηνίου του RustDesk"),
|
||||
("Ignore Battery Optimizations", "Αγνόηση βελτιστοποιήσεων μπαταρίας"),
|
||||
("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"),
|
||||
("Start on boot", "Έναρξη κατά την εκκίνηση"),
|
||||
("Start the screen sharing service on boot, requires special permissions", "Η έναρξη της υπηρεσίας κοινής χρήσης οθόνης κατά την εκκίνηση, απαιτεί ειδικά δικαιώματα"),
|
||||
@@ -315,11 +315,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Restart remote device", "Επανεκκίνηση απομακρυσμένης συσκευής"),
|
||||
("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"),
|
||||
("Restarting remote device", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής"),
|
||||
("remote_restarting_tip", "Η απομακρυσμένη συσκευή επανεκκινείται, κλείστε αυτό το μήνυμα και επανασυνδεθείτε χρησιμοποιώντας τον μόνιμο κωδικό πρόσβασης."),
|
||||
("remote_restarting_tip", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής. Κλείστε αυτό το πλαίσιο μηνύματος και επανασυνδεθείτε με τον μόνιμο κωδικό πρόσβασης μετά από λίγο."),
|
||||
("Copied", "Αντιγράφηκε"),
|
||||
("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"),
|
||||
("Fullscreen", "Πλήρης οθόνη"),
|
||||
("Mobile Actions", "Mobile Actions"),
|
||||
("Mobile Actions", "Ενέργειες για κινητά"),
|
||||
("Select Monitor", "Επιλογή οθόνης"),
|
||||
("Control Actions", "Ενέργειες ελέγχου"),
|
||||
("Display Settings", "Ρυθμίσεις οθόνης"),
|
||||
@@ -347,7 +347,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enable audio", "Ενεργοποίηση ήχου"),
|
||||
("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"),
|
||||
("Server", "Διακομιστής"),
|
||||
("Direct IP Access", "Πρόσβαση με χρήση IP"),
|
||||
("Direct IP Access", "Άμεση πρόσβαση IP"),
|
||||
("Proxy", "Διαμεσολαβητής"),
|
||||
("Apply", "Εφαρμογή"),
|
||||
("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"),
|
||||
@@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Pin Toolbar", "Καρφίτσωμα γραμμής εργαλείων"),
|
||||
("Unpin Toolbar", "Ξεκαρφίτσωμα γραμμής εργαλείων"),
|
||||
("Recording", "Εγγραφή"),
|
||||
("Directory", "Φάκελος εγγραφών"),
|
||||
("Directory", "Διαδρομή"),
|
||||
("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"),
|
||||
("Automatically record outgoing sessions", "Αυτόματη εγγραφή εξερχόμενων συνεδριών"),
|
||||
("Change", "Αλλαγή"),
|
||||
@@ -373,24 +373,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."),
|
||||
("Disconnected", "Αποσυνδέθηκε"),
|
||||
("Other", "Άλλα"),
|
||||
("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"),
|
||||
("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσουν πολλαπλές καρτέλες"),
|
||||
("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"),
|
||||
("Full Access", "Πλήρης πρόσβαση"),
|
||||
("Screen Share", "Κοινή χρήση οθόνης"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."),
|
||||
("JumpLink", "Προβολή"),
|
||||
("ubuntu-21-04-required", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."),
|
||||
("wayland-requires-higher-linux-version", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Σύνδεσμος μετάβασης"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."),
|
||||
("Show RustDesk", "Εμφάνιση RustDesk"),
|
||||
("Show RustDesk", "Εμφάνιση του RustDesk"),
|
||||
("This PC", "Αυτός ο υπολογιστής"),
|
||||
("or", "ή"),
|
||||
("Continue with", "Συνέχεια με"),
|
||||
("Elevate", "Ανύψωση"),
|
||||
("Zoom cursor", "Kέρσορας μεγέθυνσης"),
|
||||
("Zoom cursor", "Δρομέας ζουμ"),
|
||||
("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"),
|
||||
("Accept sessions via click", "Αποδοχή συνεδριών με κλικ"),
|
||||
("Accept sessions via both", "Αποδοχή συνεδριών και με τα δύο"),
|
||||
("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα συνεδρίας σας..."),
|
||||
("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα της συνεδρίας σας..."),
|
||||
("One-time Password", "Κωδικός μίας χρήσης"),
|
||||
("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"),
|
||||
("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"),
|
||||
@@ -399,27 +399,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"),
|
||||
("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."),
|
||||
("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"),
|
||||
("Skipped", "Παράλειψη"),
|
||||
("Add to address book", "Προσθήκη στο Βιβλίο Διευθύνσεων"),
|
||||
("Skipped", "Παραλήφθηκε"),
|
||||
("Add to address book", "Προσθήκη στο βιβλίο διευθύνσεων"),
|
||||
("Group", "Ομάδα"),
|
||||
("Search", "Αναζήτηση"),
|
||||
("Closed manually by web console", "Κλειστό χειροκίνητα από την κονσόλα web"),
|
||||
("Closed manually by web console", "Κλείσιμο χειροκίνητα από την κονσόλα ιστού"),
|
||||
("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"),
|
||||
("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"),
|
||||
("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."),
|
||||
("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"),
|
||||
("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"),
|
||||
("config_microphone", "Ρύθμιση μικροφώνου"),
|
||||
("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"),
|
||||
("software_render_tip", "Εάν χρησιμοποιείτε κάρτα γραφικών της Nvidia σε Linux και το παράθυρο απομακρυσμένης πρόσβασης κλείνει αμέσως μετά τη σύνδεση, η μετάβαση στο πρόγραμμα οδήγησης της Nouveau ανοιχτού κώδικα και η επιλογή χρήσης απόδοσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση του λογισμικού."),
|
||||
("Always use software rendering", "Να χρησιμοποιείτε πάντα η απόδοση λογισμικού"),
|
||||
("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με το πληκτρολόγιο, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Παρακολούθηση εισόδου\"."),
|
||||
("config_microphone", "Για να μιλήσετε εξ αποστάσεως, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή ήχου\"."),
|
||||
("request_elevation_tip", "Μπορείτε επίσης να ζητήσετε ανύψωση εάν υπάρχει κάποιος στην απομακρυσμένη πλευρά."),
|
||||
("Wait", "Περιμένετε"),
|
||||
("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"),
|
||||
("Elevation Error", "Σφάλμα ανύψωσης"),
|
||||
("Ask the remote user for authentication", "Ζητήστε από τον απομακρυσμένο χρήστη έλεγχο ταυτότητας"),
|
||||
("Choose this if the remote account is administrator", "Επιλέξτε αυτό εάν ο απομακρυσμένος λογαριασμός είναι διαχειριστής"),
|
||||
("Transmit the username and password of administrator", "Αποστολή του ονόματος χρήστη και του κωδικού πρόσβασης του διαχειριστή"),
|
||||
("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο OK στο παράθυρο UAC όπου εκτελείται το RustDesk."),
|
||||
("Request Elevation", "Αίτημα ανύψωσης δικαιωμάτων χρήστη"),
|
||||
("wait_accept_uac_tip", "Περιμένετε να αποδεχτεί ο απομακρυσμένος χρήστης το παράθυρο διαλόγου UAC."),
|
||||
("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"),
|
||||
("Transmit the username and password of administrator", "Μεταδώστε το όνομα χρήστη και τον κωδικό πρόσβασης του διαχειριστή"),
|
||||
("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο πλήκτρο Εντάξει στο παράθυρο UAC όπου εκτελείται το RustDesk."),
|
||||
("Request Elevation", "Αίτημα ανύψωσης"),
|
||||
("wait_accept_uac_tip", "Περιμένετε μέχρι ο απομακρυσμένος χρήστης να αποδεχτεί το παράθυρο διαλόγου UAC."),
|
||||
("Elevate successfully", "Επιτυχής ανύψωση"),
|
||||
("uppercase", "κεφαλαία γράμματα"),
|
||||
("lowercase", "πεζά γράμματα"),
|
||||
("digit", "αριθμός"),
|
||||
@@ -428,7 +428,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Weak", "Αδύναμο"),
|
||||
("Medium", "Μέτριο"),
|
||||
("Strong", "Δυνατό"),
|
||||
("Switch Sides", "Εναλλαγή πλευράς"),
|
||||
("Switch Sides", "Αλλαγή πλευρών"),
|
||||
("Please confirm if you want to share your desktop?", "Παρακαλώ επιβεβαιώστε αν επιθυμείτε την κοινή χρήση της επιφάνειας εργασίας;"),
|
||||
("Display", "Εμφάνιση"),
|
||||
("Default View Style", "Προκαθορισμένος τρόπος εμφάνισης"),
|
||||
@@ -442,11 +442,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Voice call", "Φωνητική κλήση"),
|
||||
("Text chat", "Συνομιλία κειμένου"),
|
||||
("Stop voice call", "Διακοπή φωνητικής κλήσης"),
|
||||
("relay_hint_tip", "Εάν δεν είναι δυνατή η απευθείας σύνδεση, μπορείτε να δοκιμάσετε να συνδεθείτε μέσω διακομιστή αναμετάδοσης"),
|
||||
("relay_hint_tip", "Ενδέχεται να μην είναι δυνατή η απευθείας σύνδεση: μπορείτε να δοκιμάσετε να συνδεθείτε μέσω αναμετάδοσης. Επιπλέον, εάν θέλετε να χρησιμοποιήσετε την αναμετάδοση στην πρώτη σας προσπάθεια, μπορείτε να προσθέσετε την \"/r\" κατάληξη στο ID ή να επιλέξετε την επιλογή \"Πάντα σύνδεση μέσω αναμετάδοσης\" στην κάρτα πρόσφατων συνεδριών, εάν υπάρχει."),
|
||||
("Reconnect", "Επανασύνδεση"),
|
||||
("Codec", "Κωδικοποίηση"),
|
||||
("Resolution", "Ανάλυση"),
|
||||
("No transfers in progress", "Δεν υπάρχει μεταφορά σε εξέλιξη"),
|
||||
("No transfers in progress", "Δεν υπάρχουν μεταφορές σε εξέλιξη"),
|
||||
("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"),
|
||||
("RDP Settings", "Ρυθμίσεις RDP"),
|
||||
("Sort by", "Ταξινόμηση κατά"),
|
||||
@@ -455,35 +455,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Minimize", "Ελαχιστοποίηση"),
|
||||
("Maximize", "Μεγιστοποίηση"),
|
||||
("Your Device", "Η συσκευή σας"),
|
||||
("empty_recent_tip", "Δεν υπάρχουν πρόσφατες συνεδρίες!\nΔοκιμάστε να ξεκινήσετε μια νέα."),
|
||||
("empty_favorite_tip", "Δεν υπάρχουν ακόμη αγαπημένες συνδέσεις;\nΑφού πραγματοποιήσετε σύνδεση με κάποιο απομακρυσμένο σταθμό, μπορείτε να τον προσθέσετε στα αγαπημένα σας!"),
|
||||
("empty_lan_tip", "Δεν έχουμε ανακαλυφθεί ακόμη απομακρυσμένοι σταθμοί."),
|
||||
("empty_address_book_tip", "Φαίνεται ότι αυτή τη στιγμή δεν υπάρχουν αγαπημένες συνδέσεις στο βιβλίο διευθύνσεών σας."),
|
||||
("empty_recent_tip", "Ωχ, δεν υπάρχουν πρόσφατες συνεδρίες!\nΏρα να προγραμματίσετε μια νέα."),
|
||||
("empty_favorite_tip", "Δεν έχετε ακόμα αγαπημένους απομακρυσμένους σταθμούς;\nΑς βρούμε κάποιον για να συνδεθούμε και ας τον προσθέσουμε στα αγαπημένα σας!"),
|
||||
("empty_lan_tip", "Ωχ όχι, φαίνεται ότι δεν έχουμε ανακαλύψει ακόμη κανέναν απομακρυσμένο σταθμό."),
|
||||
("empty_address_book_tip", "Ω, Αγαπητέ/ή μου, φαίνεται ότι αυτήν τη στιγμή δεν υπάρχουν απομακρυσμένοι σταθμοί στο βιβλίο διευθύνσεών σας."),
|
||||
("Empty Username", "Κενό όνομα χρήστη"),
|
||||
("Empty Password", "Κενός κωδικός πρόσβασης"),
|
||||
("Me", "Εγώ"),
|
||||
("identical_file_tip", "Το αρχείο είναι πανομοιότυπο με αυτό του άλλου υπολογιστή."),
|
||||
("identical_file_tip", "Αυτό το αρχείο είναι πανομοιότυπο με αυτό του απομακρυσμένου σταθμού."),
|
||||
("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"),
|
||||
("View Mode", "Λειτουργία προβολής"),
|
||||
("login_linux_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."),
|
||||
("login_linux_tip", "Πρέπει να συνδεθείτε σε έναν απομακρυσμένο λογαριασμό Linux για να ενεργοποιήσετε μια συνεδρία επιφάνειας εργασίας X"),
|
||||
("verify_rustdesk_password_tip", "Επιβεβαιώστε τον κωδικό του RustDesk"),
|
||||
("remember_account_tip", "Απομνημόνευση αυτού του λογαριασμού"),
|
||||
("os_account_desk_tip", "Αυτός ο λογαριασμός θα χρησιμοποιηθεί για την είσοδο και διαχείριση του απομακρυσμένου λειτουργικού συστήματος"),
|
||||
("os_account_desk_tip", "Αυτός ο λογαριασμός χρησιμοποιείται για σύνδεση στο απομακρυσμένο λειτουργικό σύστημα και ενεργοποίηση της συνεδρίας επιφάνειας εργασίας σε headless"),
|
||||
("OS Account", "Λογαριασμός λειτουργικού συστήματος"),
|
||||
("another_user_login_title_tip", "Υπάρχει ήδη άλλος συνδεδεμένος χρήστης"),
|
||||
("another_user_login_text_tip", "Αποσύνδεση"),
|
||||
("xorg_not_found_title_tip", "Δεν βρέθηκε το Xorg"),
|
||||
("xorg_not_found_text_tip", "Παρακαλώ εγκαταστήστε το Xorg"),
|
||||
("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμη επιφάνεια εργασίας"),
|
||||
("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμο περιβάλλον επιφάνειας εργασίας"),
|
||||
("no_desktop_text_tip", "Παρακαλώ εγκαταστήστε το περιβάλλον GNOME"),
|
||||
("No need to elevate", "Δεν χρειάζονται αυξημένα δικαιώματα"),
|
||||
("No need to elevate", "Δεν χρειάζεται ανύψωση"),
|
||||
("System Sound", "Ήχος συστήματος"),
|
||||
("Default", "Προκαθορισμένο"),
|
||||
("New RDP", "Νέα απομακρυσμένη σύνδεση"),
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", "Επιλέξτε σταθμό"),
|
||||
("New RDP", "Νέα RDP"),
|
||||
("Fingerprint", "Δακτυλικό αποτύπωμα"),
|
||||
("Copy Fingerprint", "Αντιγραφή δακτυλικού αποτυπώματος"),
|
||||
("no fingerprints", "χωρίς δακτυλικά αποτυπώματα"),
|
||||
("Select a peer", "Επιλέξτε έναν σταθμό"),
|
||||
("Select peers", "Επιλέξτε σταθμούς"),
|
||||
("Plugins", "Επεκτάσεις"),
|
||||
("Uninstall", "Κατάργηση εγκατάστασης"),
|
||||
@@ -494,10 +494,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("resolution_original_tip", "Αρχική ανάλυση"),
|
||||
("resolution_fit_local_tip", "Προσαρμογή στην τοπική ανάλυση"),
|
||||
("resolution_custom_tip", "Προσαρμοσμένη ανάλυση"),
|
||||
("Collapse toolbar", "Εμφάνιση γραμμής εργαλείων"),
|
||||
("Accept and Elevate", "Αποδοχή με αυξημένα δικαιώματα"),
|
||||
("accept_and_elevate_btn_tooltip", "Αποδοχή της σύνδεσης με αυξημένα δικαιώματα χρήστη"),
|
||||
("clipboard_wait_response_timeout_tip", "Έληξε ο χρόνος αναμονής για την ανταπόκριση της αντιγραφής"),
|
||||
("Collapse toolbar", "Σύμπτυξη γραμμής εργαλείων"),
|
||||
("Accept and Elevate", "Αποδοχή και ανύψωση"),
|
||||
("accept_and_elevate_btn_tooltip", "Αποδεχτείτε τη σύνδεση και ανυψώστε τα δικαιώματα UAC."),
|
||||
("clipboard_wait_response_timeout_tip", "Λήξη χρονικού ορίου αναμονής για απάντηση αντιγραφής."),
|
||||
("Incoming connection", "Εισερχόμενη σύνδεση"),
|
||||
("Outgoing connection", "Εξερχόμενη σύνδεση"),
|
||||
("Exit", "Έξοδος"),
|
||||
@@ -506,7 +506,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Service", "Υπηρεσία"),
|
||||
("Start", "Έναρξη"),
|
||||
("Stop", "Διακοπή"),
|
||||
("exceed_max_devices", "Υπέρβαση μέγιστου ορίου αποθηκευμένων συνδέσεων"),
|
||||
("exceed_max_devices", "Έχετε φτάσει τον μέγιστο αριθμό διαχειριζόμενων συσκευών."),
|
||||
("Sync with recent sessions", "Συγχρονισμός των πρόσφατων συνεδριών"),
|
||||
("Sort tags", "Ταξινόμηση ετικετών"),
|
||||
("Open connection in new tab", "Άνοιγμα σύνδεσης σε νέα καρτέλα"),
|
||||
@@ -515,14 +515,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Already exists", "Υπάρχει ήδη"),
|
||||
("Change Password", "Αλλαγή κωδικού"),
|
||||
("Refresh Password", "Ανανέωση κωδικού"),
|
||||
("ID", ""),
|
||||
("ID", "ID"),
|
||||
("Grid View", "Προβολή σε πλακίδια"),
|
||||
("List View", "Προβολή σε λίστα"),
|
||||
("Select", "Επιλογή"),
|
||||
("Toggle Tags", "Εναλλαγή ετικετών"),
|
||||
("pull_ab_failed_tip", "Αποτυχία ανανέωσης βιβλίου διευθύνσεων"),
|
||||
("push_ab_failed_tip", "Αποτυχία συγχρονισμού βιβλίου διευθύνσεων"),
|
||||
("synced_peer_readded_tip", "Οι συσκευές των τρεχουσών συνεδριών θα συγχρονιστούν με το βιβλίο διευθύνσεων"),
|
||||
("pull_ab_failed_tip", "Η ανανέωση του βιβλίου διευθύνσεων απέτυχε"),
|
||||
("push_ab_failed_tip", "Αποτυχία συγχρονισμού του βιβλίου διευθύνσεων με τον διακομιστή"),
|
||||
("synced_peer_readded_tip", "Οι συσκευές που υπήρχαν στις πρόσφατες συνεδρίες θα συγχρονιστούν ξανά με το βιβλίο διευθύνσεων."),
|
||||
("Change Color", "Αλλαγή χρώματος"),
|
||||
("Primary Color", "Κυρίως χρώμα"),
|
||||
("HSV Color", "Χρώμα HSV"),
|
||||
@@ -537,31 +537,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("I Agree", "Συμφωνώ"),
|
||||
("Decline", "Διαφωνώ"),
|
||||
("Timeout in minutes", "Τέλος χρόνου σε λεπτά"),
|
||||
("auto_disconnect_option_tip", "Αυτόματη αποσύνδεση απομακρυσμένης συνεδρίας έπειτα από την πάροδο του χρονικού ορίου αδράνειας "),
|
||||
("auto_disconnect_option_tip", "Αυτόματο κλείσιμο εισερχόμενων συνεδριών σε περίπτωση αδράνειας χρήστη"),
|
||||
("Connection failed due to inactivity", "Η σύνδεση τερματίστηκε έπειτα από την πάροδο του χρόνου αδράνειας"),
|
||||
("Check for software update on startup", "Έλεγχος για ενημερώσεις κατα την εκκίνηση"),
|
||||
("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε τον RustDesk Server Pro στην έκδοση {} ή νεότερη!"),
|
||||
("Check for software update on startup", "Έλεγχος για ενημερώσεις κατά την εκκίνηση"),
|
||||
("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε το RustDesk Server Pro στην έκδοση {} ή νεότερη!"),
|
||||
("pull_group_failed_tip", "Αποτυχία ανανέωσης της ομάδας"),
|
||||
("Filter by intersection", ""),
|
||||
("Filter by intersection", "Φιλτράρισμα κατά διασταύρωση"),
|
||||
("Remove wallpaper during incoming sessions", "Αφαίρεση εικόνας φόντου στις εισερχόμενες συνδέσεις"),
|
||||
("Test", "Δοκιμή"),
|
||||
("display_is_plugged_out_msg", "Η οθόνη έχει αποσυνδεθεί, επιστρέψτε στην κύρια οθόνη προβολής"),
|
||||
("display_is_plugged_out_msg", "Η οθόνη είναι αποσυνδεδεμένη από την πρίζα, μεταβείτε στην πρώτη οθόνη."),
|
||||
("No displays", "Δεν υπάρχουν οθόνες"),
|
||||
("Open in new window", "Άνοιγμα σε νέο παράθυρο"),
|
||||
("Show displays as individual windows", "Εμφάνιση οθονών σε ξεχωριστά παράθυρα"),
|
||||
("Use all my displays for the remote session", "Χρήση όλων των οθονών της απομακρυσμένης σύνδεσης"),
|
||||
("selinux_tip", "Έχετε ενεργοποιημένο το SELinux, το οποίο πιθανόν εμποδίζει την ορθή λειτουργία του RustDesk."),
|
||||
("selinux_tip", "Το SELinux είναι ενεργοποιημένο στη συσκευή σας, κάτι που ενδέχεται να εμποδίσει την σωστή λειτουργία του RustDesk ως ελεγχόμενης πλευράς."),
|
||||
("Change view", "Αλλαγή απεικόνισης"),
|
||||
("Big tiles", "Μεγάλα εικονίδια"),
|
||||
("Small tiles", "Μικρά εικονίδια"),
|
||||
("List", "Λίστα"),
|
||||
("Virtual display", "Εινονική οθόνη"),
|
||||
("Plug out all", "Αποσύνδεση όλων"),
|
||||
("True color (4:4:4)", ""),
|
||||
("True color (4:4:4)", "Αληθινό χρώμα (4:4:4)"),
|
||||
("Enable blocking user input", "Ενεργοποίηση αποκλεισμού χειρισμού από τον χρήστη"),
|
||||
("id_input_tip", "Μπορείτε να εισάγετε ενα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (<domain>:<port>).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"<id>@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."),
|
||||
("privacy_mode_impl_mag_tip", "Προφύλαξη Οθόνης"),
|
||||
("privacy_mode_impl_virtual_display_tip", "Εικονική Οθόνη"),
|
||||
("id_input_tip", "Μπορείτε να εισάγετε ένα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (<domain>:<port>).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"<id>@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."),
|
||||
("privacy_mode_impl_mag_tip", "Λειτουργία 1"),
|
||||
("privacy_mode_impl_virtual_display_tip", "Λειτουργία 2"),
|
||||
("Enter privacy mode", "Ενεργοποίηση λειτουργίας απορρήτου"),
|
||||
("Exit privacy mode", "Διακοπή λειτουργίας απορρήτου"),
|
||||
("idd_not_support_under_win10_2004_tip", "Το πρόγραμμα οδήγησης έμμεσης οθόνης δεν υποστηρίζεται. Απαιτείτε λειτουργικό σύστημα Windows 10 έκδοση 2004 ή νεότερο."),
|
||||
@@ -571,26 +571,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("swap-left-right-mouse", "Εναλλαγή αριστερό-δεξί κουμπί του ποντικιού"),
|
||||
("2FA code", "κωδικός 2FA"),
|
||||
("More", "Περισσότερα"),
|
||||
("enable-2fa-title", "Ενεργοποίηση Πιστοποίησης Δύο Παραγόντων"),
|
||||
("enable-2fa-desc", "Ρυθμίστε τον έλεγχο ταυτότητας τώρα. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως Authy, Microsoft ή Google Authenticator στο τηλέφωνο ή στην επιφάνεια εργασίας σας.Σαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."),
|
||||
("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι ο κωδικός και οι ρυθμίσεις τοπικής ώρας είναι σωστές"),
|
||||
("enable-2fa-title", "Ενεργοποίηση πιστοποίησης δύο παραγόντων"),
|
||||
("enable-2fa-desc", "Παρακαλούμε να ρυθμίστε τώρα τον έλεγχο ταυτότητας. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως το Authy, το Microsoft ή το Google Authenticator στο τηλέφωνο ή τον υπολογιστή σας.\n\nΣαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."),
|
||||
("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι οι ρυθμίσεις κωδικού και τοπικής ώρας είναι σωστές."),
|
||||
("enter-2fa-title", "Έλεγχος ταυτότητας δύο παραγόντων"),
|
||||
("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι εως 6 χαρακτήρες"),
|
||||
("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι έως 6 χαρακτήρες"),
|
||||
("2FA code must be 6 digits.", "Ο κωδικός 2FA πρέπει να είναι 6ψήφιος."),
|
||||
("Multiple Windows sessions found", ""),
|
||||
("Multiple Windows sessions found", "Βρέθηκαν πολλές συνεδρίες των Windows"),
|
||||
("Please select the session you want to connect to", "Επιλέξτε τη συνεδρία στην οποία θέλετε να συνδεθείτε"),
|
||||
("powered_by_me", "Με την υποστήριξη της RustDesk"),
|
||||
("outgoing_only_desk_tip", ""),
|
||||
("preset_password_warning", "προειδοποίηση προκαθορισμένου κωδικού πρόσβασης"),
|
||||
("powered_by_me", "Με την υποστήριξη του RustDesk"),
|
||||
("outgoing_only_desk_tip", "Αυτή είναι μια προσαρμοσμένη έκδοση.\nΜπορείτε να συνδεθείτε με άλλες συσκευές, αλλά άλλες συσκευές δεν μπορούν να συνδεθούν με τη δική σας συσκευή."),
|
||||
("preset_password_warning", "Αυτή η προσαρμοσμένη έκδοση συνοδεύεται από έναν προκαθορισμένο κωδικό πρόσβασης. Όποιος γνωρίζει αυτόν τον κωδικό πρόσβασης θα μπορούσε να αποκτήσει τον πλήρη έλεγχο της συσκευής σας. Εάν δεν το περιμένατε αυτό, απεγκαταστήστε αμέσως το λογισμικό."),
|
||||
("Security Alert", "Ειδοποίηση ασφαλείας"),
|
||||
("My address book", "Το βιβλίο διευθύνσεών μου"),
|
||||
("Personal", "Προσωπικό"),
|
||||
("Owner", "Ιδιοκτήτης"),
|
||||
("Set shared password", "Ορίστε κοινόχρηστο κωδικό πρόσβασης"),
|
||||
("Set shared password", "Ορίστε έναν κοινόχρηστο κωδικό πρόσβασης"),
|
||||
("Exist in", "Υπάρχει στο"),
|
||||
("Read-only", "Μόνο για ανάγνωση"),
|
||||
("Read/Write", "Ανάγνωση/Εγγραφή"),
|
||||
("Full Control", "Πλήρης Έλεγχος"),
|
||||
("Full Control", "Πλήρης έλεγχος"),
|
||||
("share_warning_tip", "Τα παραπάνω πεδία είναι κοινόχρηστα και ορατά σε άλλους."),
|
||||
("Everyone", "Όλοι"),
|
||||
("ab_web_console_tip", "Περισσότερα στην κονσόλα web"),
|
||||
@@ -598,18 +598,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("no_need_privacy_mode_no_physical_displays_tip", "Δεν υπάρχουν φυσικές οθόνες, δεν χρειάζεται να χρησιμοποιήσετε τη λειτουργία απορρήτου."),
|
||||
("Follow remote cursor", "Παρακολούθηση απομακρυσμένου κέρσορα"),
|
||||
("Follow remote window focus", "Παρακολούθηση απομακρυσμένου ενεργού παραθύρου"),
|
||||
("default_proxy_tip", "Προκαθορισμένο πρωτόκολλο Socks5 στην πόρτα 1080"),
|
||||
("default_proxy_tip", "Το προεπιλεγμένο πρωτόκολλο και η θύρα είναι Socks5 και 1080"),
|
||||
("no_audio_input_device_tip", "Δεν βρέθηκε συσκευή εισόδου ήχου."),
|
||||
("Incoming", "Εισερχόμενη"),
|
||||
("Outgoing", "Εξερχόμενη"),
|
||||
("Clear Wayland screen selection", ""),
|
||||
("clear_Wayland_screen_selection_tip", ""),
|
||||
("confirm_clear_Wayland_screen_selection_tip", ""),
|
||||
("android_new_voice_call_tip", ""),
|
||||
("texture_render_tip", ""),
|
||||
("Use texture rendering", ""),
|
||||
("Floating window", ""),
|
||||
("floating_window_tip", ""),
|
||||
("Clear Wayland screen selection", "Εκκαθάριση επιλογής οθόνης Wayland"),
|
||||
("clear_Wayland_screen_selection_tip", "Αφού διαγράψετε την επιλογή οθόνης, μπορείτε να επιλέξετε ξανά την οθόνη για κοινή χρήση."),
|
||||
("confirm_clear_Wayland_screen_selection_tip", "Είστε βέβαιοι ότι θέλετε να διαγράψετε την επιλογή οθόνης Wayland;"),
|
||||
("android_new_voice_call_tip", "Ελήφθη ένα νέο αίτημα φωνητικής κλήσης. Εάν το αποδεχτείτε, ο ήχος θα μεταβεί σε φωνητική επικοινωνία."),
|
||||
("texture_render_tip", "Χρησιμοποιήστε την απόδοση υφής για να κάνετε τις εικόνες πιο ομαλές. Μπορείτε να δοκιμάσετε να απενεργοποιήσετε αυτήν την επιλογή εάν αντιμετωπίσετε προβλήματα απόδοσης."),
|
||||
("Use texture rendering", "Χρήση απόδοσης υφής"),
|
||||
("Floating window", "Πλωτό παράθυρο"),
|
||||
("floating_window_tip", "Βοηθά στη διατήρηση της υπηρεσίας παρασκηνίου RustDesk"),
|
||||
("Keep screen on", "Διατήρηση οθόνης Ανοιχτή"),
|
||||
("Never", "Ποτέ"),
|
||||
("During controlled", "Κατα την διάρκεια απομακρυσμένου ελέγχου"),
|
||||
@@ -619,8 +619,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Apps", "Εφαρμογές"),
|
||||
("Volume up", "Αύξηση έντασης"),
|
||||
("Volume down", "Μείωση έντασης"),
|
||||
("Power", ""),
|
||||
("Telegram bot", ""),
|
||||
("Power", "Ενέργεια"),
|
||||
("Telegram bot", "Telegram bot"),
|
||||
("enable-bot-tip", "Εάν ενεργοποιήσετε αυτήν τη δυνατότητα, μπορείτε να λάβετε τον κωδικό 2FA από το bot σας. Μπορεί επίσης να λειτουργήσει ως ειδοποίηση σύνδεσης."),
|
||||
("enable-bot-desc", "1, Ανοίξτε μια συνομιλία με τον @BotFather., Στείλτε την εντολή \"/newbot\". Θα λάβετε ένα διακριτικό αφού ολοκληρώσετε αυτό το βήμα.3, Ξεκινήστε μια συνομιλία με το bot που μόλις δημιουργήσατε. Στείλτε ένα μήνυμα που αρχίζει με κάθετο (\"/\") όπως \"/hello\" για να το ενεργοποιήσετε."),
|
||||
("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"),
|
||||
@@ -640,11 +640,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Parent directory", "Γονικός φάκελος"),
|
||||
("Resume", "Συνέχεια"),
|
||||
("Invalid file name", "Μη έγκυρο όνομα αρχείου"),
|
||||
("one-way-file-transfer-tip", ""),
|
||||
("one-way-file-transfer-tip", "Η μονόδρομη μεταφορά αρχείων είναι ενεργοποιημένη στην ελεγχόμενη πλευρά."),
|
||||
("Authentication Required", "Απαιτείται έλεγχος ταυτότητας"),
|
||||
("Authenticate", "Πιστοποίηση"),
|
||||
("web_id_input_tip", ""),
|
||||
("Download", ""),
|
||||
("web_id_input_tip", "Μπορείτε να εισαγάγετε ένα ID στον ίδιο διακομιστή, η άμεση πρόσβαση IP δεν υποστηρίζεται στον web client.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε άλλον διακομιστή, παρακαλούμε να προσθέστε τη διεύθυνση διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε δημόσιο διακομιστή, παρακαλούμε να εισαγάγετε \"<id>@public\". Το κλειδί δεν είναι απαραίτητο για δημόσιο διακομιστή."),
|
||||
("Download", "Λήψη"),
|
||||
("Upload folder", "Μεταφόρτωση φακέλου"),
|
||||
("Upload files", "Μεταφόρτωση αρχείων"),
|
||||
("Clipboard is synchronized", "Το πρόχειρο έχει συγχρονιστεί"),
|
||||
@@ -653,88 +653,95 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"),
|
||||
("Accessible devices", "Προσβάσιμες συσκευές"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
("printer-os-requirement-tip", ""),
|
||||
("printer-requires-installed-{}-client-tip", ""),
|
||||
("printer-{}-not-installed-tip", ""),
|
||||
("printer-{}-ready-tip", ""),
|
||||
("Install {} Printer", ""),
|
||||
("Outgoing Print Jobs", ""),
|
||||
("Incoming Print Jobs", ""),
|
||||
("Incoming Print Job", ""),
|
||||
("use-the-default-printer-tip", ""),
|
||||
("use-the-selected-printer-tip", ""),
|
||||
("auto-print-tip", ""),
|
||||
("print-incoming-job-confirm-tip", ""),
|
||||
("remote-printing-disallowed-tile-tip", ""),
|
||||
("remote-printing-disallowed-text-tip", ""),
|
||||
("save-settings-tip", ""),
|
||||
("dont-show-again-tip", ""),
|
||||
("Take screenshot", ""),
|
||||
("Taking screenshot", ""),
|
||||
("screenshot-merged-screen-not-supported-tip", ""),
|
||||
("screenshot-action-tip", ""),
|
||||
("Save as", ""),
|
||||
("Copy to clipboard", ""),
|
||||
("Enable remote printer", ""),
|
||||
("Downloading {}", ""),
|
||||
("{} Update", ""),
|
||||
("{}-to-update-tip", ""),
|
||||
("download-new-version-failed-tip", ""),
|
||||
("Auto update", ""),
|
||||
("update-failed-check-msi-tip", ""),
|
||||
("websocket_tip", ""),
|
||||
("Use WebSocket", ""),
|
||||
("Trackpad speed", ""),
|
||||
("Default trackpad speed", ""),
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("d3d_render_tip", "Όταν είναι ενεργοποιημένη η απόδοση D3D, η οθόνη του τηλεχειριστηρίου ενδέχεται να είναι μαύρη σε ορισμένα μηχανήματα."),
|
||||
("Use D3D rendering", "Χρήση απόδοσης D3D"),
|
||||
("Printer", "Εκτυπωτής"),
|
||||
("printer-os-requirement-tip", "Η λειτουργία εξερχόμενης εκτύπωσης του εκτυπωτή απαιτεί Windows 10 ή νεότερη έκδοση."),
|
||||
("printer-requires-installed-{}-client-tip", "Για να χρησιμοποιήσετε την απομακρυσμένη εκτύπωση, πρέπει να εγκατασταθεί το {} σε αυτήν τη συσκευή."),
|
||||
("printer-{}-not-installed-tip", "Ο εκτυπωτής {} δεν είναι εγκατεστημένος."),
|
||||
("printer-{}-ready-tip", "Ο εκτυπωτής {} είναι εγκατεστημένος και έτοιμος για χρήση."),
|
||||
("Install {} Printer", "Εγκατάσταση εκτυπωτή {}"),
|
||||
("Outgoing Print Jobs", "Εξερχόμενες εργασίες εκτύπωσης"),
|
||||
("Incoming Print Jobs", "Εισερχόμενες εργασίες εκτύπωσης"),
|
||||
("Incoming Print Job", "Εισερχόμενη εργασία εκτύπωσης"),
|
||||
("use-the-default-printer-tip", "Χρήση του προεπιλεγμένου εκτυπωτή"),
|
||||
("use-the-selected-printer-tip", "Χρήση του επιλεγμένου εκτυπωτή"),
|
||||
("auto-print-tip", "Εκτυπώστε αυτόματα χρησιμοποιώντας τον επιλεγμένο εκτυπωτή."),
|
||||
("print-incoming-job-confirm-tip", "Λάβατε μια εργασία εκτύπωσης από απόσταση. Θέλετε να την εκτελέσετε από την πλευρά σας;"),
|
||||
("remote-printing-disallowed-tile-tip", "Η απομακρυσμένη εκτύπωση δεν επιτρέπεται"),
|
||||
("remote-printing-disallowed-text-tip", "Οι ρυθμίσεις δικαιωμάτων της ελεγχόμενης πλευράς απαγορεύουν την Απομακρυσμένη Εκτύπωση."),
|
||||
("save-settings-tip", "Αποθήκευση ρυθμίσεων"),
|
||||
("dont-show-again-tip", "Να μην εμφανιστεί ξανά αυτό"),
|
||||
("Take screenshot", "Λήψη στιγμιότυπου οθόνης"),
|
||||
("Taking screenshot", "Γίνεται λήψη στιγμιότυπου οθόνης"),
|
||||
("screenshot-merged-screen-not-supported-tip", "Η συγχώνευση στιγμιότυπων οθόνης από πολλές οθόνες δεν υποστηρίζεται προς το παρόν. Αλλάξτε σε μία μόνο οθόνη και δοκιμάστε ξανά."),
|
||||
("screenshot-action-tip", "Επιλέξτε πώς θα συνεχίσετε με το στιγμιότυπο οθόνης."),
|
||||
("Save as", "Αποθήκευση ως"),
|
||||
("Copy to clipboard", "Αντιγραφή στο πρόχειρο"),
|
||||
("Enable remote printer", "Ενεργοποίηση απομακρυσμένου εκτυπωτή"),
|
||||
("Downloading {}", "Γίνεται Λήψη {}"),
|
||||
("{} Update", "{} Ενημέρωση"),
|
||||
("{}-to-update-tip", "Το {} θα κλείσει τώρα και θα εγκαταστήσει τη νέα έκδοση."),
|
||||
("download-new-version-failed-tip", "Η λήψη απέτυχε. Μπορείτε να δοκιμάσετε ξανά ή να κάνετε κλικ στο κουμπί \"Λήψη\" για να κάνετε λήψη από τη σελίδα έκδοσης και να κάνετε αναβάθμιση χειροκίνητα."),
|
||||
("Auto update", "Αυτόματη ενημέρωση"),
|
||||
("update-failed-check-msi-tip", "Η μέθοδος εγκατάστασης απέτυχε. Κάντε κλικ στο κουμπί \"Λήψη\" για λήψη από τη σελίδα έκδοσης και κάντε χειροκίνητα την αναβάθμιση."),
|
||||
("websocket_tip", "Όταν χρησιμοποιείτε το WebSocket, υποστηρίζονται μόνο συνδέσεις αναμετάδοσης."),
|
||||
("Use WebSocket", "Χρήση WebSocket"),
|
||||
("Trackpad speed", "Ταχύτητα trackpad"),
|
||||
("Default trackpad speed", "Προεπιλεγμένη ταχύτητα trackpad"),
|
||||
("Numeric one-time password", "Αριθμητικός κωδικός πρόσβασης μίας χρήσης"),
|
||||
("Enable IPv6 P2P connection", "Ενεργοποίηση σύνδεσης IPv6 P2P"),
|
||||
("Enable UDP hole punching", "Ενεργοποίηση διάτρησης οπών UDP"),
|
||||
("View camera", "Προβολή κάμερας"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
("Terminal (Run as administrator)", ""),
|
||||
("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", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
("Scale custom", ""),
|
||||
("Custom scale slider", ""),
|
||||
("Decrease", ""),
|
||||
("Increase", ""),
|
||||
("Show virtual mouse", ""),
|
||||
("Virtual mouse size", ""),
|
||||
("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", ""),
|
||||
("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", ""),
|
||||
("Enable camera", "Ενεργοποίηση κάμερας"),
|
||||
("No cameras", "Δεν υπάρχουν κάμερες"),
|
||||
("view_camera_unsupported_tip", "Η τηλεχειριστήριο δεν υποστηρίζει την προβολή της κάμερας."),
|
||||
("Terminal", "Τερματικό"),
|
||||
("Enable terminal", "Ενεργοποίηση τερματικού"),
|
||||
("New tab", "Νέα καρτέλα"),
|
||||
("Keep terminal sessions on disconnect", "Διατήρηση περιόδων λειτουργίας τερματικού κατά την αποσύνδεση"),
|
||||
("Terminal (Run as administrator)", "Τερματικό (Εκτέλεση ως διαχειριστής)"),
|
||||
("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", "Εισαγάγετε όνομα χρήστη ή τομέα\\όνομα χρήστη"),
|
||||
("Preparing for installation ...", "Προετοιμασία για εγκατάσταση..."),
|
||||
("Show my cursor", "Εμφάνιση του κέρσορα μου"),
|
||||
("Scale custom", "Προσαρμοσμένη κλίμακα"),
|
||||
("Custom scale slider", "Ρυθμιστικό προσαρμοσμένης κλίμακας"),
|
||||
("Decrease", "Μείωση"),
|
||||
("Increase", "Αύξηση"),
|
||||
("Show virtual mouse", "Εμφάνιση εικονικού ποντικιού"),
|
||||
("Virtual mouse size", "Μέγεθος εικονικού ποντικιού"),
|
||||
("Small", "Μικρό"),
|
||||
("Large", "Μεγάλο"),
|
||||
("Show virtual joystick", "Εμφάνιση εικονικού joystick"),
|
||||
("Edit note", "Επεξεργασία σημείωσης"),
|
||||
("Alias", "Ψευδώνυμο"),
|
||||
("ScrollEdge", "Άκρη κύλισης"),
|
||||
("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", "ΣΗΜΕΙΩΣΗ: Το OSS του διακομιστή RustDesk δεν περιλαμβάνει αυτήν τη λειτουργία."),
|
||||
("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 {}", "Συνέχεια με {}"),
|
||||
("Display Name", "Εμφανιζόμενο όνομα"),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Keyboard settings"),
|
||||
("Full Access", "Full access"),
|
||||
("Screen Share", "Screen share"),
|
||||
("ubuntu-21-04-required", "Wayland requires Ubuntu 21.04 or higher version."),
|
||||
("wayland-requires-higher-linux-version", "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."),
|
||||
("xdp-portal-unavailable", "Wayland screen capture failed. The XDG Desktop Portal may have crashed or is unavailable. Try restarting it with `systemctl --user restart xdg-desktop-portal`."),
|
||||
("JumpLink", "View"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Please select the screen to be shared(Operate on the peer side)."),
|
||||
("One-time Password", "One-time password"),
|
||||
@@ -220,7 +223,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("default_proxy_tip", "Default protocol and port are Socks5 and 1080"),
|
||||
("no_audio_input_device_tip", "No audio input device found."),
|
||||
("clear_Wayland_screen_selection_tip", "After clearing the screen selection, you can reselect the screen to share."),
|
||||
("confirm_clear_Wayland_screen_selection_tip", "Are you sure to clear the Wayland screen selection?"),
|
||||
("confirm_clear_Wayland_screen_selection_tip", "Are you sure you want to clear the Wayland screen selection?"),
|
||||
("android_new_voice_call_tip", "A new voice call request was received. If you accept, the audio will switch to voice communication."),
|
||||
("texture_render_tip", "Use texture rendering to make the pictures smoother. You could try disabling this option if you encounter rendering issues."),
|
||||
("floating_window_tip", "It helps to keep RustDesk background service"),
|
||||
@@ -267,5 +270,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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"),
|
||||
("password-hidden-tip", "Permanent password is set (hidden)."),
|
||||
("preset-password-in-use-tip", "Preset password is currently in use."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", ""),
|
||||
("Full Access", ""),
|
||||
("Screen Share", ""),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."),
|
||||
("ubuntu-21-04-required", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."),
|
||||
("wayland-requires-higher-linux-version", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "View"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."),
|
||||
("Show RustDesk", ""),
|
||||
("This PC", ""),
|
||||
("or", ""),
|
||||
("Continue with", ""),
|
||||
("Elevate", ""),
|
||||
("Zoom cursor", ""),
|
||||
("Accept sessions via password", ""),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}", ""),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Ajustes de teclado"),
|
||||
("Full Access", "Acceso completo"),
|
||||
("Screen Share", "Compartir pantalla"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requiere Ubuntu 21.04 o una versión superior."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."),
|
||||
("ubuntu-21-04-required", "Wayland requiere Ubuntu 21.04 o una versión superior."),
|
||||
("wayland-requires-higher-linux-version", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Ver"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."),
|
||||
("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"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Klaviatuurisätted"),
|
||||
("Full Access", "Täielik ligipääs"),
|
||||
("Screen Share", "Ekraanijagamine"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."),
|
||||
("ubuntu-21-04-required", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."),
|
||||
("wayland-requires-higher-linux-version", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "JumpLink"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Palun vali jagatav ekraan (tegutse partneri poolel)."),
|
||||
("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"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Teklatuaren ezarpenak"),
|
||||
("Full Access", "Sarbide osoa"),
|
||||
("Screen Share", "Pantailaren partekatzea"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."),
|
||||
("ubuntu-21-04-required", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."),
|
||||
("wayland-requires-higher-linux-version", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Ikusi"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Mesedez, hautatu partekatuko den pantaila (Kudeatu parekidearen aldean)"),
|
||||
("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"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "تنظیمات صفحه کلید"),
|
||||
("Full Access", "دسترسی کامل"),
|
||||
("Screen Share", "اشتراک گذاری صفحه"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"),
|
||||
("ubuntu-21-04-required", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"),
|
||||
("wayland-requires-higher-linux-version", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "چشم انداز"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحهای را برای اشتراکگذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."),
|
||||
("Show RustDesk", "RustDesk نمایش"),
|
||||
("This PC", "This PC"),
|
||||
("or", "یا"),
|
||||
("Continue with", "ادامه با"),
|
||||
("Elevate", "ارتقاء"),
|
||||
("Zoom cursor", " بزرگنمایی نشانگر ماوس"),
|
||||
("Accept sessions via password", "قبول درخواست با رمز عبور"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}", "ادامه با {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Näppäimistöasetukset"),
|
||||
("Full Access", "Täysi käyttöoikeus"),
|
||||
("Screen Share", "Näytönjako"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."),
|
||||
("ubuntu-21-04-required", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."),
|
||||
("wayland-requires-higher-linux-version", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Pikalinkki"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Valitse jaettava näyttö (toiminto etäpäässä)."),
|
||||
("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"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].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"),
|
||||
@@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Set permanent password", "Définir le mot de passe permanent"),
|
||||
("Enable remote restart", "Activer le redémarrage à distance"),
|
||||
("Restart remote device", "Redémarrer l’appareil distant"),
|
||||
("Are you sure you want to restart", "Voulez-vous vraiment redémarrer l’appareil ?"),
|
||||
("Are you sure you want to restart", "Voulez-vous vraiment redémarrer"),
|
||||
("Restarting remote device", "Redémarrage de l’appareil distant"),
|
||||
("remote_restarting_tip", "L'appareil distant redémarre ; veuillez fermer cette boîte de dialogue et vous reconnecter en utilisant le mot de passe permanent dans quelques instants"),
|
||||
("Copied", "Copié"),
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Paramètres du clavier"),
|
||||
("Full Access", "Accès total"),
|
||||
("Screen Share", "Partage d’écran"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."),
|
||||
("ubuntu-21-04-required", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."),
|
||||
("wayland-requires-higher-linux-version", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."),
|
||||
("xdp-portal-unavailable", "Échec de la capture de l’écran Wayland. Le portail de bureau XDG a peut-être planté ou n’est pas disponible. Essayez de le redémarrer avec la commande `systemctl --user restart xdg-desktop-portal`."),
|
||||
("JumpLink", "Afficher"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l’écran à partager (côté appareil distant)."),
|
||||
("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"),
|
||||
@@ -730,11 +730,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("input note here", "saisir la note ici"),
|
||||
("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", ""),
|
||||
("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", ""),
|
||||
("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 {}"),
|
||||
("Display Name", "Nom d’affichage"),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "კლავიატურის პარამეტრები"),
|
||||
("Full Access", "სრული წვდომა"),
|
||||
("Screen Share", "ეკრანის გაზიარება"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."),
|
||||
("ubuntu-21-04-required", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."),
|
||||
("wayland-requires-higher-linux-version", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "ნახვა"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "აირჩიეთ ეკრანი გასაზიარებლად (იმუშავეთ პარტნიორის მხარეს)."),
|
||||
("Show RustDesk", "RustDesk-ის ჩვენება"),
|
||||
("This PC", "ეს კომპიუტერი"),
|
||||
("or", "ან"),
|
||||
("Continue with", "გაგრძელება"),
|
||||
("Elevate", "უფლებების აწევა"),
|
||||
("Zoom cursor", "კურსორის მასშტაბირება"),
|
||||
("Accept sessions via password", "სესიების მიღება პაროლით"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}", "{}-ით გაგრძელება"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "הגדרות מקלדת"),
|
||||
("Full Access", "גישה מלאה"),
|
||||
("Screen Share", "שיתוף מסך"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"),
|
||||
("ubuntu-21-04-required", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"),
|
||||
("wayland-requires-higher-linux-version", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "קישור מהיר"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."),
|
||||
("Show RustDesk", "הצג את RustDesk"),
|
||||
("This PC", "מחשב זה"),
|
||||
("or", "או"),
|
||||
("Continue with", "המשך עם"),
|
||||
("Elevate", "הפעל הרשאות מורחבות"),
|
||||
("Zoom cursor", "הגדל סמן"),
|
||||
("Accept sessions via password", "קבל הפעלות באמצעות סיסמה"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}", "המשך עם {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -377,14 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keyboard Settings", "Postavke tipkovnice"),
|
||||
("Full Access", "Potpuni pristup"),
|
||||
("Screen Share", "Dijeljenje zaslona"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."),
|
||||
("ubuntu-21-04-required", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"),
|
||||
("wayland-requires-higher-linux-version", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Vidi"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Molimo odaberite zaslon koji će biti podijeljen (Za rad na strani klijenta)"),
|
||||
("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"),
|
||||
@@ -736,5 +736,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 {}"),
|
||||
("Display Name", ""),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
117
src/lang/hu.rs
117
src/lang/hu.rs
@@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Password", "Jelszó"),
|
||||
("Ready", "Kész"),
|
||||
("Established", "Létrejött"),
|
||||
("connecting_status", "Kapcsolódás folyamatban…"),
|
||||
("connecting_status", "Kapcsolódás folyamatban ..."),
|
||||
("Enable service", "Szolgáltatás engedélyezése"),
|
||||
("Start service", "Szolgáltatás indítása"),
|
||||
("Service is running", "Szolgáltatás aktív"),
|
||||
@@ -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", "Azonosító-/Továbbító-kiszolgáló"),
|
||||
("ID/Relay Server", "ID/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"),
|
||||
@@ -54,7 +54,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enhancements", "Fejlesztések"),
|
||||
("Hardware Codec", "Hardveres kodek"),
|
||||
("Adaptive bitrate", "Adaptív bitráta"),
|
||||
("ID Server", "Azonosító-kiszolgáló"),
|
||||
("ID Server", "ID-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."),
|
||||
@@ -76,12 +76,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Connection Error", "Kapcsolódási hiba"),
|
||||
("Error", "Hiba"),
|
||||
("Reset by the peer", "A kapcsolatot a másik fél lezárta."),
|
||||
("Connecting...", "Kapcsolódás…"),
|
||||
("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet…"),
|
||||
("Connecting...", "Kapcsolódás..."),
|
||||
("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet ..."),
|
||||
("Please try 1 minute later", "Próbálja meg 1 perc múlva"),
|
||||
("Login Error", "Bejelentkezési hiba"),
|
||||
("Successful", "Sikeres"),
|
||||
("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra…"),
|
||||
("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra..."),
|
||||
("Name", "Név"),
|
||||
("Type", "Típus"),
|
||||
("Modified", "Módosított"),
|
||||
@@ -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", "Kapcsolat minőségének megjelenítése"),
|
||||
("Show quality monitor", "Kijelző minőségének ellenőrzé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"),
|
||||
@@ -149,9 +149,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 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"),
|
||||
("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ítse"),
|
||||
("Installation", "Telepítés"),
|
||||
("Installation Path", "Telepítési útvonal"),
|
||||
("Create start menu shortcuts", "Start menü parancsikonok létrehozása"),
|
||||
@@ -159,10 +159,10 @@ 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 ...", "Előállítás…"),
|
||||
("Generating ...", "Létrehozá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…"),
|
||||
("Listening ...", "Figyelés ..."),
|
||||
("Remote Host", "Távoli kiszolgáló"),
|
||||
("Remote Port", "Távoli port"),
|
||||
("Action", "Indítá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ájlmásolás és -beillesztés 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"),
|
||||
@@ -185,7 +185,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Relayed and unencrypted connection", "Továbbított, és nem titkosított kapcsolat"),
|
||||
("Enter Remote ID", "Távoli számítógép azonosítója"),
|
||||
("Enter your password", "Adja meg a jelszavát"),
|
||||
("Logging in...", "Belépés folyamatban…"),
|
||||
("Logging in...", "Belépés folyamatban..."),
|
||||
("Enable RDP session sharing", "RDP-munkamenet-megosztás engedélyezése"),
|
||||
("Auto Login", "Automatikus bejelentkezés"),
|
||||
("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"),
|
||||
@@ -219,7 +219,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("verification_tip", "A regisztrált e-mail-címre egy ellenőrző kód lesz elküldve. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."),
|
||||
("Logout", "Kilépés"),
|
||||
("Tags", "Címkék"),
|
||||
("Search ID", "Azonosító keresése…"),
|
||||
("Search ID", "Azonosító keresése..."),
|
||||
("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"),
|
||||
@@ -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", "Vászon mozgatása"),
|
||||
("Canvas Move", "Nézet módosítása"),
|
||||
("Pinch to Zoom", "Kétujjas nagyítás"),
|
||||
("Canvas Zoom", "Vászon nagyítása"),
|
||||
("Reset canvas", "Vászon visszaállítása"),
|
||||
("Canvas Zoom", "Nézet nagyítása"),
|
||||
("Reset canvas", "Nézet 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"),
|
||||
@@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enable remote restart", "Távoli újraindítás engedélyezése"),
|
||||
("Restart remote device", "Távoli eszköz újraindítása"),
|
||||
("Are you sure you want to restart", "Biztosan újra szeretné indítani?"),
|
||||
("Restarting remote device", "Távoli eszköz újraindítása…"),
|
||||
("Restarting remote device", "Távoli eszköz újraindítása..."),
|
||||
("remote_restarting_tip", "A távoli eszköz újraindul, zárja be ezt az üzenetet, kapcsolódjon újra az állandó jelszavával"),
|
||||
("Copied", "Másolva"),
|
||||
("Exit Fullscreen", "Kilépés teljes képernyős módból"),
|
||||
@@ -369,28 +369,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Deny LAN discovery", "Felfedezés tiltása"),
|
||||
("Write a message", "Üzenet írása"),
|
||||
("Prompt", "Kérés"),
|
||||
("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére…"),
|
||||
("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére..."),
|
||||
("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövőbeni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."),
|
||||
("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űzetbeá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."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."),
|
||||
("ubuntu-21-04-required", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."),
|
||||
("wayland-requires-higher-linux-version", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."),
|
||||
("xdp-portal-unavailable", ""),
|
||||
("JumpLink", "Hiperhivatkozás"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Válassza ki a megosztani kívánt képernyőt."),
|
||||
("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"),
|
||||
("Accept sessions via click", "Munkamenetek elfogadása kattintással"),
|
||||
("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"),
|
||||
("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét…"),
|
||||
("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét..."),
|
||||
("One-time Password", "Egyszer használatos jelszó"),
|
||||
("Use one-time password", "Használjon ideiglenes jelszót"),
|
||||
("One-time password length", "Egyszer használatos jelszó hossza"),
|
||||
@@ -408,15 +408,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 +442,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", "Saját eszköz"),
|
||||
("Your Device", "Az én eszközöm"),
|
||||
("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 +469,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 +490,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"),
|
||||
@@ -559,7 +559,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 +569,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."),
|
||||
@@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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"),
|
||||
("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"),
|
||||
@@ -643,18 +643,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", "Kliens vágólapjának frissítése"),
|
||||
("Update client clipboard", "Az ügyfél 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"),
|
||||
@@ -673,7 +673,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("save-settings-tip", "Beállítások mentése"),
|
||||
("dont-show-again-tip", "Ne jelenítse meg újra"),
|
||||
("Take screenshot", "Képernyőkép készítése"),
|
||||
("Taking screenshot", "Képernyőkép készítése…"),
|
||||
("Taking screenshot", "Képernyőkép készítése..."),
|
||||
("screenshot-merged-screen-not-supported-tip", "Egyesített képernyőről nem támogatott a képernyőkép készítése"),
|
||||
("screenshot-action-tip", "Képernyőkép-művelet"),
|
||||
("Save as", "Mentés másként"),
|
||||
@@ -681,10 +681,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enable remote printer", "Távoli nyomtatók engedélyezése"),
|
||||
("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."),
|
||||
("{}-to-update-tip", "{} 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. 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"),
|
||||
@@ -708,7 +708,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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"),
|
||||
("Preparing for installation ...", "Felkészülés a telepítésre…"),
|
||||
("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"),
|
||||
@@ -727,14 +727,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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 bevitele"),
|
||||
("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"),
|
||||
("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", ""),
|
||||
("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", ""),
|
||||
("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 következő 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 ezzel: {}"),
|
||||
("Display Name", "Kijelző név"),
|
||||
("password-hidden-tip", ""),
|
||||
("preset-password-in-use-tip", ""),
|
||||
].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