mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-18 22:59:26 +08:00
Compare commits
283 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26e5f7bbeb | ||
|
|
7a3e67e1d3 | ||
|
|
af53b1e8c9 | ||
|
|
9db7217cab | ||
|
|
d0651e32c5 | ||
|
|
0646a5b313 | ||
|
|
e9692b94ca | ||
|
|
6e62c10fa0 | ||
|
|
52bfc02eea | ||
|
|
2282c8e308 | ||
|
|
9409912344 | ||
|
|
2afd538cf1 | ||
|
|
ab48f10f25 | ||
|
|
1b40d146ee | ||
|
|
b4e13706bd | ||
|
|
f2473974b8 | ||
|
|
50fc6d691f | ||
|
|
247f0b7eb1 | ||
|
|
80c4a83a39 | ||
|
|
3fb3d51567 | ||
|
|
596e7b33db | ||
|
|
c01bbeea78 | ||
|
|
47886c4068 | ||
|
|
348c477f75 | ||
|
|
61194182eb | ||
|
|
9bca5ac000 | ||
|
|
b65ef36049 | ||
|
|
391ef70007 | ||
|
|
9bcfe9d148 | ||
|
|
94e23a6cd0 | ||
|
|
55ddb9751a | ||
|
|
9d82ef1a22 | ||
|
|
555bb66668 | ||
|
|
1581272104 | ||
|
|
a37f4d79db | ||
|
|
4723d07215 | ||
|
|
3177786219 | ||
|
|
061dc9962d | ||
|
|
0a62103ccd | ||
|
|
2e2b4ac2fe | ||
|
|
e91f4fc104 | ||
|
|
bdd3bb946e | ||
|
|
398b0d8d8b | ||
|
|
dc41495566 | ||
|
|
effbb45eb7 | ||
|
|
4d960c3c8c | ||
|
|
475bef63d7 | ||
|
|
e711f73451 | ||
|
|
661be6ae36 | ||
|
|
e31b04b6a7 | ||
|
|
d5eb87ee8b | ||
|
|
65c721e088 | ||
|
|
69af5f2fa6 | ||
|
|
abb7748ee9 | ||
|
|
8d559725d5 | ||
|
|
8c68b83265 | ||
|
|
ae255c83ee | ||
|
|
856362006a | ||
|
|
331b624cd6 | ||
|
|
0117e94e6f | ||
|
|
aa680533ae | ||
|
|
94e76c3b6f | ||
|
|
0258b9adca | ||
|
|
f15b9f05fb | ||
|
|
dd7a124334 | ||
|
|
7447a36782 | ||
|
|
e2830347e6 | ||
|
|
9389f3306d | ||
|
|
f3819e19d4 | ||
|
|
9caf0dddc3 | ||
|
|
f766d28c36 | ||
|
|
7ad3023285 | ||
|
|
86e79b0162 | ||
|
|
09098e86ca | ||
|
|
7ce13a21f8 | ||
|
|
cf0d090c08 | ||
|
|
f26d2a7b84 | ||
|
|
5faf0ad3cf | ||
|
|
ee5cdc3155 | ||
|
|
e0f5fa39f3 | ||
|
|
d21a1023d2 | ||
|
|
884373794a | ||
|
|
9060f9ec8a | ||
|
|
fd4e0146e1 | ||
|
|
58fd2d3ccd | ||
|
|
bb6e080c1c | ||
|
|
7b7c93b78d | ||
|
|
94ae3886c5 | ||
|
|
79c6da98d2 | ||
|
|
18ea3a4b59 | ||
|
|
2ae7f00ceb | ||
|
|
4d8bfab86e | ||
|
|
50b1c02243 | ||
|
|
fa61693ccd | ||
|
|
7822d3d923 | ||
|
|
98d99fae64 | ||
|
|
7330dc70f3 | ||
|
|
46cd090f98 | ||
|
|
d6ba063655 | ||
|
|
590ecc43ff | ||
|
|
1eee03818d | ||
|
|
5dd15d1282 | ||
|
|
8754579181 | ||
|
|
57b826c56b | ||
|
|
bfd6ca79f8 | ||
|
|
d84b26a9cd | ||
|
|
181b3afc2d | ||
|
|
31934e9bd8 | ||
|
|
14a8f00e5b | ||
|
|
44e00f8ec2 | ||
|
|
645a76d43f | ||
|
|
bf77f582d0 | ||
|
|
c58fd145f2 | ||
|
|
a5a3352655 | ||
|
|
2533493c66 | ||
|
|
832458c59e | ||
|
|
5beebf967d | ||
|
|
4e9bdcbc1f | ||
|
|
070b0354fd | ||
|
|
f9405711c6 | ||
|
|
7792ac1481 | ||
|
|
05a812247a | ||
|
|
645cfd3b3d | ||
|
|
294ffcd9d3 | ||
|
|
738afb54d7 | ||
|
|
83f45b2212 | ||
|
|
8b2643e060 | ||
|
|
e79724644d | ||
|
|
861fc91578 | ||
|
|
fa7770d901 | ||
|
|
e0f35b9046 | ||
|
|
32e96e3705 | ||
|
|
d52d9da043 | ||
|
|
68eaedfddc | ||
|
|
e08cf3c0eb | ||
|
|
f919f297ac | ||
|
|
c39c49fd17 | ||
|
|
90cb0ee56d | ||
|
|
edab44afdf | ||
|
|
ec0456e606 | ||
|
|
4e1a814aeb | ||
|
|
4f8f34ec01 | ||
|
|
836950354b | ||
|
|
527be17eaf | ||
|
|
a0f4984ba5 | ||
|
|
4121e3fd14 | ||
|
|
a7a2f77ea3 | ||
|
|
46622f7576 | ||
|
|
45c9c505db | ||
|
|
777c25bba2 | ||
|
|
01146574f2 | ||
|
|
39151531d7 | ||
|
|
a5fefaddf5 | ||
|
|
f68d333bf1 | ||
|
|
3c028fe5b5 | ||
|
|
6ff679c6b4 | ||
|
|
48da2709d7 | ||
|
|
042d031a04 | ||
|
|
b2d5eb9714 | ||
|
|
511a0b3693 | ||
|
|
06ab987e32 | ||
|
|
b4a30cac73 | ||
|
|
f801c251ed | ||
|
|
d3d7b09fe7 | ||
|
|
6144a1c97e | ||
|
|
118552ad0e | ||
|
|
9217205229 | ||
|
|
4f6ae08110 | ||
|
|
90ad55d4aa | ||
|
|
f1a4494e3c | ||
|
|
5fa17e440a | ||
|
|
a73fa3cbf6 | ||
|
|
ae7faea6d5 | ||
|
|
b525185d7f | ||
|
|
dad841e493 | ||
|
|
550dd5ad72 | ||
|
|
b4eeaee737 | ||
|
|
cee69bb8b4 | ||
|
|
43501b663e | ||
|
|
9c0711e1db | ||
|
|
d00b8bb580 | ||
|
|
c735fbd54c | ||
|
|
9d0d729522 | ||
|
|
4c354ee1ae | ||
|
|
f56c5c1bbb | ||
|
|
a615b5e119 | ||
|
|
12a9745b88 | ||
|
|
b05a77ece2 | ||
|
|
ea106354af | ||
|
|
e6aefcfa30 | ||
|
|
4c5ec42100 | ||
|
|
f61728e24c | ||
|
|
2d7d1d0545 | ||
|
|
968a9deee5 | ||
|
|
e79f254e50 | ||
|
|
8f712a51a3 | ||
|
|
7d20e0f26f | ||
|
|
c1b46b6b9d | ||
|
|
dd0e6c31ba | ||
|
|
54cf1c8225 | ||
|
|
a73be6fc94 | ||
|
|
2c976eb1e2 | ||
|
|
9dbb6217f7 | ||
|
|
1a8e3005cd | ||
|
|
e9b4e4d170 | ||
|
|
fb1661c897 | ||
|
|
ca7b4872d9 | ||
|
|
9475743b4e | ||
|
|
86bbdf7a5d | ||
|
|
4f6818477f | ||
|
|
d46862e47d | ||
|
|
61cdb60362 | ||
|
|
419bb3f0b0 | ||
|
|
0869ceb5da | ||
|
|
36e52e41ad | ||
|
|
bd85e9c322 | ||
|
|
6ffbcd1375 | ||
|
|
a7d0f3b149 | ||
|
|
5e60a47408 | ||
|
|
aa30f68c05 | ||
|
|
eee5b5f64c | ||
|
|
5298a5f83b | ||
|
|
d56df22838 | ||
|
|
ca00706a38 | ||
|
|
62276b4f4f | ||
|
|
e55722308e | ||
|
|
7c8d2daaf6 | ||
|
|
04e2792f5f | ||
|
|
7196dbed6e | ||
|
|
ec1de6413a | ||
|
|
d30ead1d96 | ||
|
|
20fcddffbd | ||
|
|
83aae23ba6 | ||
|
|
df847e9a60 | ||
|
|
2ad1c907b8 | ||
|
|
c626c2414d | ||
|
|
2864e1984a | ||
|
|
f0c5580f57 | ||
|
|
abde556695 | ||
|
|
c9d5e15ac0 | ||
|
|
16e9e716b6 | ||
|
|
f438bf582b | ||
|
|
c0789a5fc0 | ||
|
|
198967ea35 | ||
|
|
279fb72a4f | ||
|
|
5c2538e7af | ||
|
|
296aa7f8a0 | ||
|
|
2cb096178a | ||
|
|
57ee031827 | ||
|
|
52b6541dd0 | ||
|
|
86d9e62780 | ||
|
|
a8c822ee5d | ||
|
|
bc1f629c17 | ||
|
|
66a9882e30 | ||
|
|
978ead4c42 | ||
|
|
56010344b7 | ||
|
|
c853dd4279 | ||
|
|
f1f504f9f1 | ||
|
|
f34f962b73 | ||
|
|
ded19ce5b9 | ||
|
|
3f9ba53dca | ||
|
|
3e82b99f8e | ||
|
|
98e9e2a0e8 | ||
|
|
375cede605 | ||
|
|
838decccc4 | ||
|
|
b7742ff806 | ||
|
|
36815e9a02 | ||
|
|
581313341b | ||
|
|
d9109560a7 | ||
|
|
d7dc49f1f7 | ||
|
|
f9af3e3a0c | ||
|
|
3b73ee3a23 | ||
|
|
d8eb23a571 | ||
|
|
cc0761446f | ||
|
|
d972c0eda1 | ||
|
|
2d403913b5 | ||
|
|
62a83ad319 | ||
|
|
a7aacc7855 | ||
|
|
9ddeab9be2 | ||
|
|
d808bb2947 | ||
|
|
c19f33a137 | ||
|
|
11d3ea5f24 | ||
|
|
ca1b35440b |
56
.claude/commands/reflection.md
Normal file
56
.claude/commands/reflection.md
Normal file
@@ -0,0 +1,56 @@
|
||||
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.
|
||||
6
.github/workflows/bridge.yml
vendored
6
.github/workflows/bridge.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
os: ubuntu-22.04,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
@@ -40,9 +40,9 @@ jobs:
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libclang-10-dev \
|
||||
libclang-dev \
|
||||
libgtk-3-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -4,9 +4,8 @@ env:
|
||||
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.11.16
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
|
||||
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -82,7 +81,7 @@ jobs:
|
||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-22.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
@@ -112,6 +111,7 @@ jobs:
|
||||
g++ \
|
||||
libpam0g-dev \
|
||||
libasound2-dev \
|
||||
libunwind-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
|
||||
76
.github/workflows/flutter-build.yml
vendored
76
.github/workflows/flutter-build.yml
vendored
@@ -23,7 +23,7 @@ env:
|
||||
MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81
|
||||
CARGO_NDK_VERSION: "3.1.2"
|
||||
SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
|
||||
SCITER_NASM_DEBVERSION: "2.14-1"
|
||||
SCITER_NASM_DEBVERSION: "2.15.05-1"
|
||||
LLVM_VERSION: "15.0.6"
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
ANDROID_FLUTTER_VERSION: "3.24.5"
|
||||
@@ -32,8 +32,13 @@ env:
|
||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2025.01.13
|
||||
# If we change the `VCPKG COMMIT_ID`, please remember:
|
||||
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
|
||||
# Or we may face build issue like
|
||||
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
|
||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
|
||||
VERSION: "1.3.9"
|
||||
VERSION: "1.4.1"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -172,24 +177,24 @@ jobs:
|
||||
|
||||
# Download printer driver files and extract them to ./rustdesk
|
||||
try {
|
||||
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4.zip -OutFile rustdesk_printer_driver_v4.zip
|
||||
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip
|
||||
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip
|
||||
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums
|
||||
|
||||
# Check and move the files
|
||||
$checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4\.zip$').Matches.Groups[1].Value
|
||||
$downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4.zip -Algorithm SHA256
|
||||
$checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value
|
||||
$downloadsum_dll = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256
|
||||
if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) {
|
||||
Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file."
|
||||
Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath .
|
||||
$checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4-1.4\.zip$').Matches.Groups[1].Value
|
||||
$downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256
|
||||
$checksum_adapter = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value
|
||||
$downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256
|
||||
if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) {
|
||||
Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file."
|
||||
Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath .
|
||||
mkdir ./rustdesk/drivers
|
||||
mv -Force .\rustdesk_printer_driver_v4 ./rustdesk/drivers/RustDeskPrinterDriver
|
||||
mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver
|
||||
Expand-Archive printer_driver_adapter.zip -DestinationPath .
|
||||
mv -Force .\printer_driver_adapter.dll ./rustdesk
|
||||
} elseif ($checksum_driver -ne $downloadsum_driver.Hash) {
|
||||
Write-Output "rustdesk_printer_driver_v4, checksums do not match, ignore the file."
|
||||
Write-Output "rustdesk_printer_driver_v4-1.4, checksums do not match, ignore the file."
|
||||
} else {
|
||||
Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file."
|
||||
}
|
||||
@@ -924,21 +929,21 @@ jobs:
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-20.04,
|
||||
os: ubuntu-24.04,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
- {
|
||||
arch: armv7,
|
||||
target: armv7-linux-androideabi,
|
||||
os: ubuntu-20.04,
|
||||
os: ubuntu-24.04,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-linux-android,
|
||||
os: ubuntu-20.04,
|
||||
os: ubuntu-24.04,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
@@ -975,7 +980,8 @@ jobs:
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-10-dev \
|
||||
libclang-dev \
|
||||
libunwind-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
@@ -987,7 +993,7 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-17-jdk-headless \
|
||||
@@ -1206,7 +1212,7 @@ jobs:
|
||||
needs: [build-rustdesk-android]
|
||||
name: build rustdesk android universal apk
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
reltype: release
|
||||
x86_target: "" # can be ",android-x86"
|
||||
@@ -1244,7 +1250,8 @@ jobs:
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-10-dev \
|
||||
libclang-dev \
|
||||
libunwind-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
@@ -1256,7 +1263,7 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-17-jdk-headless \
|
||||
@@ -1396,7 +1403,7 @@ jobs:
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
distro: ubuntu18.04,
|
||||
on: ubuntu-20.04,
|
||||
on: ubuntu-22.04,
|
||||
deb_arch: amd64,
|
||||
vcpkg-triplet: x64-linux,
|
||||
}
|
||||
@@ -1731,7 +1738,7 @@ jobs:
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
on: ubuntu-20.04,
|
||||
on: ubuntu-22.04,
|
||||
distro: ubuntu18.04,
|
||||
deb_arch: amd64,
|
||||
sciter_arch: x64,
|
||||
@@ -1939,7 +1946,7 @@ jobs:
|
||||
build-appimage:
|
||||
name: Build appimage ${{ matrix.job.target }}
|
||||
needs: [build-rustdesk-linux]
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1968,13 +1975,11 @@ jobs:
|
||||
run: |
|
||||
# install libarchive-tools for bsdtar command used in AppImageBuilder.yml
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libarchive-tools
|
||||
# https://github.com/AppImage/AppImageKit/wiki/FUSE
|
||||
sudo apt-get install -y libarchive-tools libfuse2
|
||||
# set-up appimage-builder
|
||||
pushd /tmp
|
||||
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
||||
chmod +x appimage-builder-x86_64.AppImage
|
||||
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
|
||||
popd
|
||||
# https://github.com/AppImage/AppImageKit/issues/1395
|
||||
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
|
||||
# run appimage-builder
|
||||
pushd appimage
|
||||
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
|
||||
@@ -2001,15 +2006,16 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
distro: ubuntu18.04,
|
||||
on: ubuntu-20.04,
|
||||
# https://github.com/ostreedev/ostree/commit/4bac96a8c817beda37448f9b8c662162bb619981
|
||||
distro: ubuntu22.04,
|
||||
on: ubuntu-22.04,
|
||||
arch: x86_64,
|
||||
suffix: "",
|
||||
}
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
distro: ubuntu18.04,
|
||||
on: ubuntu-20.04,
|
||||
distro: ubuntu22.04,
|
||||
on: ubuntu-22.04,
|
||||
arch: x86_64,
|
||||
suffix: "-sciter",
|
||||
}
|
||||
@@ -2075,7 +2081,9 @@ jobs:
|
||||
build-rustdesk-web:
|
||||
if: False
|
||||
name: build-rustdesk-web
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
env:
|
||||
|
||||
12
.github/workflows/playground.yml
vendored
12
.github/workflows/playground.yml
vendored
@@ -16,9 +16,8 @@ env:
|
||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.11.16
|
||||
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
|
||||
VERSION: "1.3.9"
|
||||
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
|
||||
VERSION: "1.4.1"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -242,7 +241,7 @@ jobs:
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-20.04,
|
||||
os: ubuntu-22.04,
|
||||
openssl-arch: android-arm64,
|
||||
ref: master, # latest
|
||||
}
|
||||
@@ -267,7 +266,8 @@ jobs:
|
||||
libayatana-appindicator3-dev\
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-10-dev \
|
||||
libclang-dev \
|
||||
libunwind-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
@@ -280,7 +280,7 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
yasm \
|
||||
ninja-build \
|
||||
|
||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -2,6 +2,7 @@ name: Publish to WinGet
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -9,5 +10,6 @@ jobs:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
version: "1.4.1"
|
||||
release-tag: "1.4.1"
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
91
CLAUDE.md
Normal file
91
CLAUDE.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Build Commands
|
||||
- `cargo run` - Build and run the desktop application (requires libsciter library)
|
||||
- `python3 build.py --flutter` - Build Flutter version (desktop)
|
||||
- `python3 build.py --flutter --release` - Build Flutter version in release mode
|
||||
- `python3 build.py --hwcodec` - Build with hardware codec support
|
||||
- `python3 build.py --vram` - Build with VRAM feature (Windows only)
|
||||
- `cargo build --release` - Build Rust binary in release mode
|
||||
- `cargo build --features hwcodec` - Build with specific features
|
||||
|
||||
### Flutter Mobile Commands
|
||||
- `cd flutter && flutter build android` - Build Android APK
|
||||
- `cd flutter && flutter build ios` - Build iOS app
|
||||
- `cd flutter && flutter run` - Run Flutter app in development mode
|
||||
- `cd flutter && flutter test` - Run Flutter tests
|
||||
|
||||
### Testing
|
||||
- `cargo test` - Run Rust tests
|
||||
- `cd flutter && flutter test` - Run Flutter tests
|
||||
|
||||
### Platform-Specific Build Scripts
|
||||
- `flutter/build_android.sh` - Android build script
|
||||
- `flutter/build_ios.sh` - iOS build script
|
||||
- `flutter/build_fdroid.sh` - F-Droid build script
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Directory Structure
|
||||
- **`src/`** - Main Rust application code
|
||||
- `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead)
|
||||
- `src/server/` - Audio/clipboard/input/video services and network connections
|
||||
- `src/client.rs` - Peer connection handling
|
||||
- `src/platform/` - Platform-specific code
|
||||
- **`flutter/`** - Flutter UI code for desktop and mobile
|
||||
- **`libs/`** - Core libraries
|
||||
- `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities
|
||||
- `libs/scrap/` - Screen capture functionality
|
||||
- `libs/enigo/` - Platform-specific keyboard/mouse control
|
||||
- `libs/clipboard/` - Cross-platform clipboard implementation
|
||||
|
||||
### Key Components
|
||||
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
|
||||
- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
|
||||
- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
|
||||
- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
|
||||
- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
|
||||
|
||||
### UI Architecture
|
||||
- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
|
||||
- **Modern UI**: Flutter-based - files in `flutter/`
|
||||
- Desktop: `flutter/lib/desktop/`
|
||||
- Mobile: `flutter/lib/mobile/`
|
||||
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
|
||||
|
||||
## Important Build Notes
|
||||
|
||||
### Dependencies
|
||||
- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom`
|
||||
- Set `VCPKG_ROOT` environment variable
|
||||
- Download appropriate Sciter library for legacy UI support
|
||||
|
||||
### Ignore Patterns
|
||||
When working with files, ignore these directories:
|
||||
- `target/` - Rust build artifacts
|
||||
- `flutter/build/` - Flutter build output
|
||||
- `flutter/.dart_tool/` - Flutter tooling files
|
||||
|
||||
### Cross-Platform Considerations
|
||||
- Windows builds require additional DLLs and virtual display drivers
|
||||
- macOS builds need proper signing and notarization for distribution
|
||||
- Linux builds support multiple package formats (deb, rpm, AppImage)
|
||||
- Mobile builds require platform-specific toolchains (Android SDK, Xcode)
|
||||
|
||||
### Feature Flags
|
||||
- `hwcodec` - Hardware video encoding/decoding
|
||||
- `vram` - VRAM optimization (Windows only)
|
||||
- `flutter` - Enable Flutter UI
|
||||
- `unix-file-copy-paste` - Unix file clipboard support
|
||||
- `screencapturekit` - macOS ScreenCaptureKit (macOS only)
|
||||
|
||||
### Config
|
||||
All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types:
|
||||
- Settings
|
||||
- Local
|
||||
- Display
|
||||
- Built-in
|
||||
1564
Cargo.lock
generated
1564
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.3.9"
|
||||
version = "1.4.1"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -46,7 +46,6 @@ screencapturekit = ["cpal/screencapturekit"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
whoami = "1.5.0"
|
||||
scrap = { path = "libs/scrap", features = ["wayland"] }
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
serde_derive = "1.0"
|
||||
@@ -82,7 +81,8 @@ fon = "0.6"
|
||||
zip = "0.6"
|
||||
shutdown_hooks = "0.1"
|
||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||
|
||||
stunclient = "0.4"
|
||||
kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"}
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
|
||||
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
|
||||
@@ -95,9 +95,10 @@ sys-locale = "0.3"
|
||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||
clipboard = { path = "libs/clipboard" }
|
||||
ctrlc = "3.2"
|
||||
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
|
||||
# arboard = { version = "3.4", features = ["wayland-data-control"] }
|
||||
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
|
||||
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||
portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" }
|
||||
|
||||
system_shutdown = "4.0"
|
||||
qrcode-generator = "4.1"
|
||||
@@ -118,13 +119,20 @@ winapi = { version = "0.3", features = [
|
||||
"ioapiset",
|
||||
"winspool",
|
||||
] }
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32",
|
||||
"Win32_System",
|
||||
"Win32_System_Diagnostics",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
] }
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
virtual_display = { path = "libs/virtual_display" }
|
||||
remote_printer = { path = "libs/remote_printer" }
|
||||
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
|
||||
shared_memory = "0.12"
|
||||
tauri-winrt-notification = "0.1.2"
|
||||
tauri-winrt-notification = "0.1"
|
||||
runas = "1.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
@@ -172,6 +180,7 @@ once_cell = {version = "1.18", optional = true}
|
||||
nix = { version = "0.29", features = ["term", "process"]}
|
||||
gtk = "0.18"
|
||||
termios = "0.3"
|
||||
terminfo = "0.8"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
@@ -215,7 +224,3 @@ panic = 'abort'
|
||||
strip = true
|
||||
#opt-level = 'z' # only have smaller size after strip
|
||||
rpath = true
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = '...' # Platform-specific.
|
||||
#strip = "debuginfo"
|
||||
|
||||
12
README.md
12
README.md
@@ -13,11 +13,11 @@
|
||||
> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application.
|
||||
|
||||
|
||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Yet another remote desktop solution, written in Rust. Works out of the box with no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
@@ -46,7 +46,7 @@ Please download Sciter dynamic library yourself.
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Raw steps to build
|
||||
## Raw Steps to build
|
||||
|
||||
- Prepare your Rust development env and C++ build env
|
||||
|
||||
@@ -59,7 +59,7 @@ Please download Sciter dynamic library yourself.
|
||||
|
||||
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## How to build on Linux
|
||||
## How to Build on Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
@@ -117,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
|
||||
@@ -154,7 +154,7 @@ Or, if you're running a release executable:
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application might not be able to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
|
||||
Please ensure that you run these commands from the root of the RustDesk repository, or the application may not find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host.
|
||||
|
||||
## File Structure
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.9
|
||||
version: 1.4.1
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -99,3 +99,4 @@ AppDir:
|
||||
AppImage:
|
||||
arch: aarch64
|
||||
update-information: guess
|
||||
comp: gzip
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.9
|
||||
version: 1.4.1
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -102,3 +102,4 @@ AppDir:
|
||||
AppImage:
|
||||
arch: x86_64
|
||||
update-information: guess
|
||||
comp: gzip
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
# Beitr<EFBFBD>ge zu RustDesk
|
||||
# Beiträge zu RustDesk
|
||||
|
||||
RustDesk begr<EFBFBD><EFBFBD>t Beitr<EFBFBD>ge von jedem. Hier sind die Richtlinien, wenn Sie uns
|
||||
helfen m<EFBFBD>chten:
|
||||
RustDesk begrüßt Beiträge von jedem. Hier sind die Richtlinien, wenn Sie uns
|
||||
helfen möchten:
|
||||
|
||||
## Beitr<EFBFBD>ge
|
||||
## Beiträge
|
||||
|
||||
Beitr<EFBFBD>ge zu RustDesk oder seinen Abh<EFBFBD>ngigkeiten sollten in Form von Pull
|
||||
Beiträge zu RustDesk oder seinen Abhängigkeiten sollten in Form von Pull
|
||||
Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur
|
||||
(jemand mit der Erlaubnis, Korrekturen einzubringen) gepr<EFBFBD>ft und entweder in den
|
||||
Hauptbaum eingef<EFBFBD>gt oder Feedback f<EFBFBD>r notwendige <EFBFBD>nderungen gegeben. Alle
|
||||
Beitr<EFBFBD>ge sollten diesem Format folgen, auch die von Hauptakteuren.
|
||||
(jemand mit der Erlaubnis, Korrekturen einzubringen) geprüft und entweder in den
|
||||
Hauptbaum eingefügt oder Feedback für notwendige Änderungen gegeben. Alle
|
||||
Beiträge sollten diesem Format folgen, auch die von Hauptakteuren.
|
||||
|
||||
Wenn Sie an einem Problem arbeiten m<EFBFBD>chten, melden Sie es bitte zuerst an, indem
|
||||
Sie auf GitHub erkl<EFBFBD>ren, dass Sie daran arbeiten m<EFBFBD>chten. Damit soll verhindert
|
||||
werden, dass Beitr<EFBFBD>ge zum gleichen Thema doppelt bearbeitet werden.
|
||||
Wenn Sie an einem Problem arbeiten möchten, melden Sie es bitte zuerst an, indem
|
||||
Sie auf GitHub erklären, dass Sie daran arbeiten möchten. Damit soll verhindert
|
||||
werden, dass Beiträge zum gleichen Thema doppelt bearbeitet werden.
|
||||
|
||||
## Checkliste f<EFBFBD>r Pull Requests
|
||||
## Checkliste für Pull Requests
|
||||
|
||||
- Verzweigen Sie sich vom Master-Branch und, falls n<EFBFBD>tig, wechseln Sie zum
|
||||
- Verzweigen Sie sich vom Master-Branch und, falls nötig, wechseln Sie zum
|
||||
aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das
|
||||
Zusammenf<EFBFBD>hren mit dem Master nicht reibungslos funktioniert, werden Sie
|
||||
m<EFBFBD>glicherweise aufgefordert, Ihre <EFBFBD>nderungen zu <EFBFBD>berarbeiten.
|
||||
Zusammenführen mit dem Master nicht reibungslos funktioniert, werden Sie
|
||||
möglicherweise aufgefordert, Ihre Änderungen zu überarbeiten.
|
||||
|
||||
- Commits sollten so klein wie m<EFBFBD>glich sein und gleichzeitig sicherstellen, dass
|
||||
jeder Commit unabh<EFBFBD>ngig voneinander korrekt ist (d. h., jeder Commit sollte
|
||||
sich <EFBFBD>bersetzen lassen und Tests bestehen).
|
||||
- Commits sollten so klein wie möglich sein und gleichzeitig sicherstellen, dass
|
||||
jeder Commit unabhängig voneinander korrekt ist (d. h., jeder Commit sollte
|
||||
sich übersetzen lassen und Tests bestehen).
|
||||
|
||||
- Commits sollten von einem "Herkunftszertifikat f<EFBFBD>r Entwickler"
|
||||
- Commits sollten von einem "Herkunftszertifikat für Entwickler"
|
||||
(https://developercertificate.org) begleitet werden, das besagt, dass Sie (und
|
||||
ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE)
|
||||
einverstanden sind. In Git ist dies die Option `-s` f<EFBFBD>r `git commit`.
|
||||
einverstanden sind. In Git ist dies die Option `-s` für `git commit`.
|
||||
|
||||
- Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur
|
||||
Begutachtung ben<EFBFBD>tigen, k<EFBFBD>nnen Sie einem Gutachter mit @ antworten und um eine
|
||||
Begutachtung des Pull Requests oder einen Kommentar bitten. Sie k<EFBFBD>nnen auch
|
||||
Begutachtung benötigen, können Sie einem Gutachter mit @ antworten und um eine
|
||||
Begutachtung des Pull Requests oder einen Kommentar bitten. Sie können auch
|
||||
per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten.
|
||||
|
||||
- F<EFBFBD>gen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue
|
||||
- Fügen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue
|
||||
Funktion beziehen.
|
||||
|
||||
Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||
@@ -47,4 +47,4 @@ https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
|
||||
|
||||
## Kommunikation
|
||||
|
||||
RustDesk-Mitarbeiter arbeiten h<EFBFBD>ufig im [Discord](https://discord.gg/nDceKgxnkV).
|
||||
RustDesk-Mitarbeiter arbeiten häufig im [Discord](https://discord.gg/nDceKgxnkV).
|
||||
|
||||
@@ -1,35 +1,41 @@
|
||||
# RustDesk 기여 가이드라인
|
||||
# RustDesk 기여하기
|
||||
|
||||
RustDesk는 모든 사람의 기여를 환영합니다. 만약 RustDesk에 기여하고 싶다면 아래 가이드를 참고해주세요:
|
||||
RustDesk는 모든 분들의 참여를 환영합니다. 저희를 도와주실 생각이 있으시다면
|
||||
다음 지침을 따르세요:
|
||||
|
||||
## 기여 방식
|
||||
## 기여
|
||||
|
||||
RustDesk 또는 종속성에 대한 기여는 GitHub Pull Request 형태로 이루어져야 합니다.
|
||||
모든 Pull Request는 코어 기여자가 검토하며, 메인 저장소에 반영되거나 필요한 수정 사항에 대한 피드백을 받습니다.
|
||||
모든 기여는 이 형식을 따라야 합니다.
|
||||
RustDesk 또는 그 종속성에 대한 기여는 GitHub 풀 리퀘스트 형태로
|
||||
이루어져야 합니다. 각 풀 리퀘스트는 핵심 기여자 (패치 적용 권한이
|
||||
있는 사람)가 검토하여 메인 트리에 추가하거나 필요한 변경 사항에
|
||||
대한 피드백을 제공합니다. 핵심 기여자의 기여를 포함하여 모든 기여는
|
||||
이 형식을 따라야 합니다.
|
||||
|
||||
특정 이슈에 작업하고 싶다면, 먼저 GitHub 이슈에 댓글을 달아 작업하겠다고 알려주세요.
|
||||
이는 동일한 작업에 대해 중복 기여가 발생하는 것을 방지하기 위함입니다.
|
||||
이슈에 대해 작업하고 싶으시면 먼저 해당 이슈에 대해 작업하고 싶다는
|
||||
댓글을 달아 해당 이슈를 요청하세요. 이는 동일한 이슈에 대한 기여자의
|
||||
중복된 노력을 방지하기 위한 것입니다.
|
||||
|
||||
## Pull Request Checklist
|
||||
## 풀 리퀘스트 체크리스트
|
||||
|
||||
- master 브랜치에서 새 브랜치를 생성하고 작업하세요.<br/>
|
||||
필요한 경우 PR 제출 전에 최신 master 브랜치에 리베이스(rebase)하세요.<br/>
|
||||
충돌이 발생하면 기여자가 직접 해결해야 합니다.
|
||||
- Master 브랜치에서 브랜치를 만들고, 필요한 경우 풀 리퀘스트를 제출하기
|
||||
전에 현재 마스터 브랜치로 리베이스하세요. 마스터 브랜치와 깔끔하게
|
||||
병합되지 않으면 변경 사항을 리베이스하라는 요청을 받을 수 있습니다.
|
||||
|
||||
- 커밋은 가능한 한 작고 독립적인 단위로 작성하세요.<br/>
|
||||
각 커밋은 독립적으로 빌드와 테스트를 통과해야 합니다.
|
||||
- 커밋은 가능한 한 작아야 하지만, 각 커밋이 독립적으로 올바른지 확인
|
||||
해야 합니다 (즉, 각 커밋은 컴파일되어 테스트를 통과해야 함).
|
||||
|
||||
- 커밋에는 반드시 Developer Certificate of Origin (http://developercertificate.org) 서명이 포함되어야 합니다.<br/>
|
||||
이는 기여자(및 소속된 고용주가 있을 경우) 가 [프로젝트 라이선스](../LICENCE) 에 동의함을 나타냅니다.<br/>
|
||||
Git에서는 `git commit` 명령어에 `-s` 옵션을 사용해 서명을 추가할 수 있습니다.
|
||||
- 커밋에는 개발자 출처 증명서 (http://developercertificate.org)
|
||||
서명이 첨부되어야 하며, 이는 귀하 (및 해당되는 경우 고용주)가
|
||||
[프로젝트 라이선스](../LICENCE). 조건에 구속되는 데 동의한다는 것을 나타냅니다.
|
||||
git에서는 `git commit`에 `-s` 옵션입니다
|
||||
|
||||
- PR이 검토되지 않거나 특정 리뷰어가 필요하면,
|
||||
<br/> PR이나 댓글에서 리뷰어를 태그하거나 [이메일](mailto:info@rustdesk.com)로 리뷰를 요청할 수 있습니다.
|
||||
- 패치가 검토되지 않거나 특정인이 검토해야 하는 경우, 풀 리퀘스트나
|
||||
댓글에서 검토자에게 @-답글을 보내 검토를 요청하거나
|
||||
[이메일](mailto:info@rustdesk.com)을 통해 검토를 요청할 수 있습니다.
|
||||
|
||||
- 수정된 버그나 추가된 기능과 관련된 테스트 코드를 포함해주세요.
|
||||
- 수정된 버그 또는 새 기능과 관련된 테스트를 추가합니다.
|
||||
|
||||
Git 사용에 대한 자세한 내용은 [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)을 참조하세요.
|
||||
구체적인 git 지침은, [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)을 참조하세요.
|
||||
|
||||
## 행동 강령
|
||||
|
||||
|
||||
@@ -5,18 +5,14 @@ RustDesk приветствует вклад каждого.
|
||||
|
||||
## Вклад в развитие
|
||||
|
||||
Вклады в развитие RustDesk или его зависимости должны быть
|
||||
сделаны в виде `pull request` на GitHub. Каждый такой
|
||||
`pull request` будет рассмотрен основным участником
|
||||
(кем-то, у кого есть разрешение на влив исправлений)
|
||||
и либо помещен в основное дерево, либо Вам будет дан отзыв
|
||||
о необходимых правках. Все материалы должны соответствовать
|
||||
этому формату, даже те, которые поступают от основных авторов.
|
||||
Вклады в развитие RustDesk или его зависимости должны быть сделаны в виде `pull request` на GitHub.
|
||||
Каждый такой `pull request` будет рассмотрен основным участником (кем-то, у кого есть разрешение
|
||||
на влив исправлений) и либо помещен в основное дерево, либо Вам будет дан отзыв о необходимых правках.
|
||||
Все материалы должны соответствовать этому формату, даже те, которые поступают от основных авторов.
|
||||
|
||||
Если вы хотите поработать над какой-либо проблемой, то пожалуйста,
|
||||
сначала напишите об этом, создав тикет на GitHub, и описав,
|
||||
над чем вы хотите поработать. Это делается для того, чтобы
|
||||
предотвратить дублирование усилий участников по одному и тому же вопросу.
|
||||
Если вы хотите поработать над какой-либо проблемой, то пожалуйста, сначала напишите об этом,
|
||||
создав `issue` на GitHub, и описав, над чем вы хотите поработать. Это делается для того,
|
||||
чтобы предотвратить дублирование усилий участников по одному и тому же вопросу.
|
||||
|
||||
## Контрольный список для Ваших `pull request`
|
||||
|
||||
@@ -24,13 +20,13 @@ RustDesk приветствует вклад каждого.
|
||||
ветку перед отправкой `pull request`. При наличии конфликтов слияния вам будет
|
||||
предложено их устранить, возможно при помощи того же `rebase`.
|
||||
|
||||
- Коммиты должны быть, по возможности, небольшим, при этом гарантируя, что каждаый
|
||||
- Коммиты должны быть, по возможности, небольшими, при этом гарантируя, что каждый
|
||||
коммит является независимо правильным (т.е., каждый коммит должен компилироваться и проходить тесты).
|
||||
|
||||
- Коммиты должны сопровождаться `Developer Certificate of Origin`
|
||||
(http://developercertificate.org) подписью, которая укажет на то, что вы (и
|
||||
ваш работодатель, если это применимо) согласны соблюдать условия
|
||||
[лицензии проекта](../LICENCE). В `git` это флаг `-s` при использовании `git commit`
|
||||
- Коммиты должны сопровождаться подписью `Developer Certificate of Origin`
|
||||
(http://developercertificate.org), которая укажет на то, что вы (и ваш работодатель,
|
||||
если это применимо) согласны соблюдать условия [лицензии проекта](../LICENCE).
|
||||
В `git` это флаг `-s` при использовании `git commit`
|
||||
|
||||
- Если ваш патч не проходит рецензирование или вам нужно,
|
||||
чтобы его проверил конкретный человек, Вы можете ответить рецензенту через `@`,
|
||||
@@ -40,7 +36,7 @@ RustDesk приветствует вклад каждого.
|
||||
|
||||
Для получения конкретных инструкций `git` см. [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow).
|
||||
|
||||
## Кодекс поведения участников и вкладчиков
|
||||
## Правила поведения участников и вкладчиков
|
||||
|
||||
Нормы поведения внутри сообщества подробно описаны [здесь](CODE_OF_CONDUCT-RU.md).
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Bin<69>rprogramm im Debug-Modus erstellt.
|
||||
|
||||
Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an.
|
||||
|
||||
Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgef<65>hrt werden m<>ssen, um bestimmte Builds zu erstellen.
|
||||
|
||||
Kommando|Build-Typ|Modus
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|release
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
Dopo l'avvio di devcontainer nel contenitore docker, viene creato un binario linux in modalità debug.
|
||||
|
||||
Attualmente devcontainer consente creazione build Linux e Android sia in modalità debug che in modalità rilascio.
|
||||
|
||||
Di seguito è riportata la tabella dei comandi da eseguire dalla root del progetto per la creazione di build specifiche.
|
||||
|
||||
Comando|Tipo build|Modo
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|release
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
docker コンテナで devcontainer を起動すると、デバッグモードの linux バイナリが作成されます。
|
||||
|
||||
現在 devcontainer では、Linux と android のビルドをデバッグモードとリリースモードの両方で提供しています。
|
||||
|
||||
以下は、特定のビルドを作成するためにプロジェクトのルートから実行するコマンドの表になります。
|
||||
|
||||
コマンド|ビルド タイプ|モード
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|release
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
Na de start van devcontainer in docker container wordt een linux binaire in foutmodus aangemaakt.
|
||||
|
||||
Momenteel biedt devcontainer linux en android builds in zowel foutopsporing- als uitgave modus.
|
||||
|
||||
Hieronder staat de tabel met commando's die vanuit de root van het project moeten worden
|
||||
uitgevoerd om specifieke builds te maken.
|
||||
|
||||
Commando|Build Type|Modus
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|debug
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
Etter start av devcontainer i docker konteineren, blir en linux binærfil i debug modus laget.
|
||||
|
||||
Nå tilbyr devcontainer linux og android builds i både debug og release modus.
|
||||
|
||||
Under er tabellen over kommandoer som kan kjøres fra rot-direktive for kreasjon av spesefike builds.
|
||||
|
||||
Kommando|Build Type|Modus
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|release
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
Po uruchomieniu devcontainer w kontenerze docker, tworzony jest plik binarny linux w trybue debugowania.
|
||||
|
||||
Obecnie devcontainer oferuje kompilowanie wersji dla linux i android w obu trybach - debugowania i wersji finalnej.
|
||||
|
||||
Poniżej tabela poleceń do uruchomienia z głównego folderu do tworzenia wybranych kompilacji.
|
||||
|
||||
Polecenie|Typ kompilacji|Tryb
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|debug
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
Docker konteynerinde devcontainer'ın başlatılmasından sonra, hata ayıklama modunda bir Linux ikili dosyası oluşturulur.
|
||||
|
||||
Şu anda devcontainer, hata ayıklama ve sürüm modunda hem Linux hem de Android derlemeleri sunmaktadır.
|
||||
|
||||
Aşağıda, belirli derlemeler oluşturmak için projenin kökünden çalıştırılması gereken komutlar yer almaktadır.
|
||||
|
||||
Komut | Derleme Türü | Mod
|
||||
-|-|-
|
||||
`.devcontainer/build.sh --debug linux` | Linux | hata ayıklama
|
||||
`.devcontainer/build.sh --release linux` | Linux | sürüm
|
||||
`.devcontainer/build.sh --debug android` | Android-arm64 | hata ayıklama
|
||||
`.devcontainer/build.sh --release android` | Android-arm64 | sürüm
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
After the start of devcontainer in docker container, a linux binary in debug mode is created.
|
||||
|
||||
Currently devcontainer offers linux and android builds in both debug and release mode.
|
||||
|
||||
Below is the table on commands to run from root of the project for creating specific builds.
|
||||
|
||||
Command|Build Type|Mode
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|release
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b> لغتك الأم, <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> و <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>, README نحن بحاجة إلى مساعدتك لترجمة هذا </b>
|
||||
</p>
|
||||
|
||||
[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) :تواصل معنا عبر
|
||||
[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) :تواصل معنا عبر
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
|
||||
## التبعيات
|
||||
|
||||
لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Potřebujeme Vaši pomoc s překladem tohoto README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b>
|
||||
</p>
|
||||
|
||||
Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<b>Vi har brug for din hjælp til at oversætte denne README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href=" https://github.com/rustdesk/doc.rustdesk.com">Dokument</a> til dit modersmål</b>
|
||||
</p>
|
||||
|
||||
Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Endnu en fjernskrivebordssoftware, skrevet i Rust. Fungerer ud af æsken, ingen konfiguration påkrævet. Du har fuld kontrol over dine data uden bekymringer om sikkerhed. Du kan bruge vores rendezvous/relay-server, [opsætte din egen](https://rustdesk.com/server), eller [skrive din egen rendezvous/relay-server](https://github.com/rustdesk/rustdesk- server-demo).
|
||||
|
||||
RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for at få hjælp til at komme i gang.
|
||||
RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) for at få hjælp til at komme i gang.
|
||||
|
||||
[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
> 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.
|
||||
|
||||
|
||||
Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Ni bezonas helpon traduki tiun README kaj <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">la interfacon</a> al via denaska lingvo</b>
|
||||
</p>
|
||||
|
||||
Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,12 +9,18 @@
|
||||
<b>Necesitamos tu ayuda para traducir este README a tu idioma</b>
|
||||
</p>
|
||||
|
||||
Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
> [!Caution]
|
||||
> **Descargo de responsabilidad por mal uso:** <br>
|
||||
> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El mal uso, como el acceso no autorizado, el control o la invasión de la privacidad, va estrictamente en contra de nuestras directrices. Los autores no se hacen responsables de ningún uso indebido de la aplicación.
|
||||
|
||||
Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de tus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [instalar el tuyo](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda para empezar.
|
||||
|
||||
[**¿Cómo funciona rustdesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
@@ -24,12 +30,15 @@ RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`
|
||||
[<img src="https://fdroid.gitlab.io/artwork/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)
|
||||
|
||||
## Dependencias
|
||||
|
||||
La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter.
|
||||
Las versiones de escritorio utilizan Flutter o Sciter (obsoleto) para GUI, este tutorial es sólo para Sciter, ya que es más fácil y más amigable para empezar. Echa un vistazo a nuestro [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) para la construcción de la versión Flutter.
|
||||
|
||||
Por favor descarga la librería dinámica de Sciter tu mismo.
|
||||
Por favor descarga la librería dinámica de Sciter tú mismo.
|
||||
|
||||
[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) |
|
||||
@@ -51,13 +60,21 @@ Por favor descarga la librería dinámica de Sciter tu mismo.
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y 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
|
||||
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 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 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)
|
||||
@@ -96,12 +113,12 @@ 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
|
||||
mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Como compilar con Docker
|
||||
@@ -111,10 +128,11 @@ Empieza clonando el repositorio y compilando el contenedor de docker:
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
git submodule update --init --recursive
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando:
|
||||
Entonces, cada vez que necesites compilar la aplicación, ejecuta el siguiente comando:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
@@ -153,10 +171,10 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del
|
||||
|
||||
## Capturas de pantalla
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<p align="center" dir="auto">[<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-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]</p>
|
||||
<p dir="rtl" align="center"><b>برای ترجمه این سند (README)، <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang" dir="rtl">رابط کاربری RustDesk</a>، <a href="https://github.com/rustdesk/doc.rustdesk.com" dir="rtl">و مستندات آن</a> به زبان مادری شما به کمکتان نیازمندیم. </b></p>
|
||||
|
||||
با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
|
||||
با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi</b>
|
||||
</p>
|
||||
|
||||
Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle</b>.
|
||||
</p>
|
||||
|
||||
Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Χρειαζόμαστε τη βοήθειά σας για να μεταφράσουμε αυτό το αρχείο README, το <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> και το <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> στη μητρική σας γλώσσα</b>
|
||||
</p>
|
||||
|
||||
Επικοινωνήστε μαζί μας μέσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Επικοινωνήστε μαζί μας μέσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||

|
||||
|
||||
Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε.
|
||||
Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε.
|
||||
|
||||
[**Συχνές ερωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Kell a segítséged, hogy lefordítsuk ezt a README-t, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">a RustDesk UI-t</a> és a <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentációt</a> az anyanyelvedre</b>
|
||||
</p>
|
||||
|
||||
Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Kami membutuhkan bantuanmu untuk menterjemahkan file README dan <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ke Bahasa Indonesia</b>
|
||||
</p>
|
||||
|
||||
Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Abbiamo bisogno del tuo aiuto per tradurre questo file README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI RustDesk</a> nella tua lingua nativa</b>
|
||||
</p>
|
||||
|
||||
Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</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>]<br>
|
||||
[<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-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
|
||||
<b>READMEや<a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>、 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a>の翻訳者を歓迎します!</b>
|
||||
</p>
|
||||
|
||||
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -18,7 +18,7 @@ Rustで書かれた、設定不要ですぐに使えるリモートデスクト
|
||||

|
||||
|
||||
RustDeskは皆さんの貢献を歓迎します。
|
||||
貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。
|
||||
貢献の方法については[CONTRIBUTING.md](CONTRIBUTING.md)をご確認ください。
|
||||
|
||||
[**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
|
||||
@@ -1,64 +1,84 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#raw-steps-to-build">빌드</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</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-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b>
|
||||
<a href="#file-structure">구조</a> •
|
||||
<a href="#snapshot">스냇샷</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-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>이 README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> 및 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk 문서</a>를 귀하의 모국어로 번역하는 데 도움이 필요합니다</b>
|
||||
</p>
|
||||
|
||||
채팅하기: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
> [!Caution]
|
||||
> **오용 면책 조항:** <br>
|
||||
> RustDesk의 개발자는 이 소프트웨어의 비윤리적 또는 불법적인 사용을 묵인하거나 지원하지 않습니다. 무단 액세스, 제어 또는 개인정보 침해와 같은 오용은 엄격하게 당사의 지침에 위배됩니다. 작성자는 응용 프로그램의 오용에 대해 책임을 지지 않습니다.
|
||||
|
||||
|
||||
우리와 채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [직접 설정](https://rustdesk.com/server)하거나 [직접 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Rust로 작성된 또 다른 원격 데스크톱 소프트웨어입니다. 구성할 필요 없이 바로 사용할 수 있습니다. 보안에 대한 걱정 없이 데이터를 완벽하게 제어할 수 있습니다. 저희의 rendezvous/relay server 서버를 사용하거나, [직접 설정](https://rustdesk.com/server), 또는 [직접 rendezvous/relay 서버를 작성할 수 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/CONTRIBUTING.md`](CONTRIBUTING.md)를 참조해주세요.
|
||||
RustDesk는 모든 분들의 기여를 환영합니다. 시작하는 데 도움이 필요하면 [CONTRIBUTING-KR.md](CONTRIBUTING-KR.md)를 참조하세요.
|
||||
|
||||
[**RustDesk는 어떻게 작동하는가?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
[**자주 묻는 질문**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**바이너리 다운로드**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## 의존관계
|
||||
[**개발자 빌드**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
데스크탑판에는 GUI에 [sciter](https://sciter.com/)가 사용되었습니다. sciter dynamic library 를 다운로드해주세요.
|
||||
[<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)
|
||||
|
||||
## 종속성
|
||||
|
||||
데스크톱 버전은 GUI로 Flutter 또는 Sciter (더 이상 지원되지 않음)를 사용하며, 이 자습서는 시작하기 더 쉽고 친숙한 Sciter 전용입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)을 확인하세요.
|
||||
|
||||
Sciter 동적 라이브러리를 직접 다운로드하세요.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
모바일 버전은 Flutter를 사용합니다. 데스크탑 또한 Sciter에서 Flutter로 마이그레이션할 예정입니다.
|
||||
## 빌드를 위한 원시 단계
|
||||
|
||||
## 빌드 순서
|
||||
- Rust 개발 환경과 C++ 빌드 환경을 준비합니다
|
||||
|
||||
- Rust 개발환경, C++ 빌드 환경을 준비합니다.
|
||||
|
||||
- [vcpkg](https://github.com/microsoft/vcpkg) 설치하고 `VCPKG_ROOT` 환경변수를 정확히 설정합니다.
|
||||
- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
||||
- Linux/macOS: vcpkg install libvpx libyuv opus aom
|
||||
|
||||
- 실행 `cargo run`
|
||||
- `cargo run` 실행
|
||||
|
||||
## [빌드](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Linux에서 빌드 순서
|
||||
## Linux에서 빌드하는 방법
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y 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
|
||||
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 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 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)
|
||||
@@ -79,7 +99,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### libvpx 수정 (For Fedora용)
|
||||
### libvpx 수정 (Fedora용)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
@@ -97,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
|
||||
@@ -105,60 +125,58 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Docker에 빌드하는 방법
|
||||
## Docker로 빌드하는 방법
|
||||
|
||||
리포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
|
||||
먼저 리포지토리를 복제하고 Docker 컨테이너를 빌드합니다:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
git submodule update --init --recursive
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
이후, 애플리케이션을 빌드할 필요가 있을 때마다, 아래의의 명령을 실행합니다.
|
||||
그런 다음 응용 프로그램을 빌드해야 할 때마다 다음 명령을 실행합니다:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 명령에 다른 인수를 지정할 필요가 있다면, 명령 끝에 있는 `<OPTIONAL-ARGS>` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 명령 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 명령으로 실행할 수 있습니다.
|
||||
첫 번째 빌드는 종속성이 캐시되기까지 시간이 오래 걸릴 수 있으며, 이후 빌드는 더 빨라집니다. 또한 빌드 명령에 다른 인수를 지정해야 하는 경우 명령 끝의 `<OPTIONAL-ARGS>` 위치에 인수를 지정할 수 있습니다. 예를 들어 최적화된 릴리스 버전을 빌드하려면 위의 명령 뒤에 `--release`를 추가하면 됩니다. 결과 실행 파일은 시스템의 대상 폴더에서 사용할 수 있으며 실행할 수 있습니다::
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
혹은 출시용 실행 파일을 실행할 수도 있습니다.
|
||||
또는 릴리스 실행 파일을 실행하는 경우:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
명령을 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 하위 명령은 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다.
|
||||
RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는지 확인하세요. 그렇지 않으면 응용 프로그램이 필요한 리소스를 찾지 못할 수 있습니다. 또한 `install` 또는 `run` 과 같은 다른 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방법을 통해 지원되지 않는다는 점에 유의하세요.
|
||||
|
||||
## 파일 구조
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 설정, tcp/udp 랩퍼, protobuf, 파일 전송을 위한 fs 함수, 그 외 유틸리티 함수
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫폼 고유 키보드/마우스 컨트롤
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오, 클립보드, 입력, 비디오 서비스 그리고 네트워크 연결
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 접속 시작
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다이렉트 (TCP hole punching) 혹은 relayed 접속
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼 고유의 코드
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 구성, tcp/udp wrapper, protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡쳐
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫폼별 키보드/마우스 제어
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows, Linux, macOS용 파일 복사 및 붙여넣기 구현
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 더 이상 사용되지 않는 Sciter UI (지원 중단)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오/클립보드/입력/비디오 서비스 및 네트워크 연결
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 연결 시작
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기
|
||||
- **[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)**: Flutter 웹 클라이언트용 JavaScript
|
||||
|
||||
> [!주의]
|
||||
> **오용에 대한 면책 조항:** <br>
|
||||
> RustDesk의 개발자들은 이 소프트웨어의 비윤리적이거나 불법적인 사용을 용인하거나 지원하지 않습니다. 무단 접근, 제어 또는 개인정보 침해와 같은 오용은 우리의 지침을 엄격히 위반하는 것입니다. 개발자들은 애플리케이션의 오용에 대해 책임을 지지 않습니다.
|
||||
## 스크린샷
|
||||
|
||||
## 스냅샷
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്</b>
|
||||
</p>
|
||||
|
||||
ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Wij hebben uw hulp nodig om dit README bestand te vertalen, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> en <a href="https://github.com/rustdesk/doc.rustdesk.com">Doc</a> naar uw moedertaal</b>
|
||||
</p>
|
||||
|
||||
Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Vi trenger din hjelp til å oversette denne README-en, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> tid ditt morsmål</b>
|
||||
</p>
|
||||
|
||||
Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -17,7 +17,7 @@ Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av
|
||||
|
||||

|
||||
|
||||
RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](docs/CONTRIBUTING-NO.md) for hjelp med oppstart.
|
||||
RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](CONTRIBUTING-NO.md) for hjelp med oppstart.
|
||||
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język</b>
|
||||
</p>
|
||||
|
||||
Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b>
|
||||
</p>
|
||||
|
||||
Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -1,42 +1,52 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Ваш удаленый рабочий стол"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
<a href="#первичные-шаги-для-сборки">Первичные шаги для сборки</a> •
|
||||
<a href="#как-собрать-с-помощью-Docker">Как собрать с помощью Docker</a> •
|
||||
<a href="#структура-файлов">Структура файлов</a> •
|
||||
<a href="#скриншоты">Скриншоты</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-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Нам нужна ваша помощь для перевода этого README <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>
|
||||
и документацию RustDesk на ваш родной язык. <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a></b>
|
||||
<b>Нам нужна ваша помощь в переводе этого README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">интерфейса RustDesk</a>
|
||||
и <a href="https://github.com/rustdesk/doc.rustdesk.com">документации RustDesk</a> на ваш родной язык.</b>
|
||||
</p>
|
||||
|
||||
Общение с нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
> [!Caution]
|
||||
> **Отказ от ответственности за неправомерное использование** <br>
|
||||
> Разработчики RustDesk не одобряют и не поддерживают какое-либо неэтичное или незаконное использование данного программного обеспечения. Неправомерное использование (несанкционированный доступ, контроль или вторжение в частную жизнь) строго противоречит нашим правилам. Авторы не несут ответственности за любое неправомерное использование приложения.
|
||||
|
||||
Общение с нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Еще одно программное обеспечение для удаленного рабочего стола, написанное на Rust. Работает из коробки, не требует настройки. Вы полностью контролируете свои данные, не беспокоясь о безопасности. Вы можете использовать наш сервер ретрансляции, [настроить свой собственный](https://rustdesk.com/server), или [написать свой](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Ещё одно программное обеспечение для удаленного рабочего стола, написанное на Rust. Работает из коробки, настройки не требует. Вы полностью контролируете свои данные, не беспокоясь о безопасности. Вы можете использовать наш сервер ретрансляции, [настроить свой собственный](https://rustdesk.com/server), или [написать свой](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk приветствует вклад каждого. Ознакомьтесь с [`docs/CONTRIBUTING-RU.md`](CONTRIBUTING-RU.md) в начале работы для понимания.
|
||||
|
||||
[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) (Документация на английском языке)
|
||||
|
||||
[**Часто задаваемые вопросы**](https://github.com/rustdesk/rustdesk/wiki/FAQ) (Страница на английском языке)
|
||||
|
||||
[**СКАЧАТЬ ПРИЛОЖЕНИЕ**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**ночные сборки (актуальные)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
[**НОЧНЫЕ СБОРКИ (Актуальные)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/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://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)
|
||||
|
||||
## Зависимости
|
||||
|
||||
Настольные версии используют [sciter](https://sciter.com/) для графического интерфейса, загрузите динамическую библиотеку sciter самостоятельно.
|
||||
Для ПК-версии используются библиотеки Flutter или Sciter (устаревшее) для графического интерфейса. Данное руководство подразумевает работу с Sciter, так как он более простой в использовании и с ним легче начать работу. Вы можете также посмотреть на механизм нашего [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) для сборок на Flutter.
|
||||
|
||||
Загрузите динамическую библиотеку Flutter самостоятельно.
|
||||
|
||||
[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) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
Мобильные версии используют Flutter. В будущем мы перенесем настольную версию со Sciter на Flutter.
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Первичные шаги для сборки
|
||||
|
||||
@@ -45,22 +55,32 @@ RustDesk приветствует вклад каждого. Ознакомьт
|
||||
- Установите [vcpkg](https://github.com/microsoft/vcpkg), и правильно установите переменную `VCPKG_ROOT`
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
||||
- Linux/macOS: vcpkg install libvpx libyuv opus aom
|
||||
|
||||
- Запустите `cargo run`
|
||||
- Выполните команду `cargo run`
|
||||
|
||||
## [Сборка](https://rustdesk.com/docs/ru/dev/build/)
|
||||
|
||||
## Как собрать на Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y 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
|
||||
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 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 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)
|
||||
@@ -99,7 +119,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
|
||||
@@ -114,16 +134,17 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
git submodule update --init --recursive
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Затем каждый раз, когда вам нужно собрать приложение, запускайте следующую команду:
|
||||
Затем при каждой сборке приложения выполняйте следующую команду:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
Обратите внимание, что первая сборка может занять больше времени, прежде чем зависимости будут кэшированы, но последующие сборки будут выполняться быстрее. Кроме того, если вам нужно указать другие аргументы для команды сборки, вы можете сделать это в конце команды в переменной `<OPTIONAL-ARGS>`. Например, если вы хотите создать оптимизированную версию, вы должны запустить приведенную выше команду и в конце строки добавить `--release`. Полученный исполняемый файл будет доступен в целевой папке вашей системы и может быть запущен с помощью:
|
||||
Обратите внимание, что первая сборка может занять больше времени, прежде чем зависимости будут кэшированы, но последующие сборки будут выполняться быстрее. Кроме того, если вам нужно указать другие аргументы для команды сборки, вы можете сделать это в конце команды в переменной `<OPTIONAL-ARGS>`. Например, если вы хотите создать оптимизированную версию, вы должны выполнить приведенную выше команду и в конце строки добавить `--release`. Полученный исполняемый файл будет доступен в целевой папке вашей системы и может быть запущен с помощью следующей команды:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
@@ -135,29 +156,28 @@ target/debug/rustdesk
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Пожалуйста, убедитесь, что вы запускаете эти команды из корня репозитория RustDesk, иначе приложение не сможет найти необходимые ресурсы. Также обратите внимание, что другие cargo подкоманды, такие как `install` или `run`, в настоящее время не поддерживаются этим методом, поскольку они будут устанавливать или запускать программу внутри контейнера, а не на хосте.
|
||||
Пожалуйста, убедитесь, что вы запускаете эти команды из корня репозитория RustDesk, иначе приложение не сможет найти необходимые ресурсы. Также обратите внимание, что другие подкоманды Cargo, такие как `install` или `run`, в настоящее время не поддерживаются этим методом, поскольку они будут устанавливать или запускать программу внутри контейнера, а не на хосте.
|
||||
|
||||
## Структура файлов
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфиг, обертка tcp/udp, protobuf, функции fs для передачи файлов и некоторые другие служебные функции
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфигурация, враппер TCP/UDP, protobuf, функции файловой системы для передачи файлов и некоторые другие служебные функции
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захват экрана
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: специфичное для платформы управление клавиатурой/мышью
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервисы аудио/буфера обмена/ввода/видео и сетевых подключений
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: функционал буфера обмена файлами для Windows, Linux, и macOS
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс на Sciter (устаревшее)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервисы аудио, буфера обмена, ввода, видео и сетевых подключений
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое соединение
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: свяжитесь с [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дождитесь удаленного прямого (обход TCP NAT) или ретранслируемого соединения
|
||||
- **[src/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)**: специфичный для платформы код
|
||||
|
||||
> [!Осторожно]
|
||||
> **Отказ от ответственности за неправомерное использование:** <br>
|
||||
> Разработчики RustDesk не одобряют и не поддерживают какое-либо неэтичное или незаконное использование данного программного обеспечения. Неправомерное использование, такое как несанкционированный доступ, контроль или вторжение в частную жизнь, строго противоречит нашим правилам. Авторы не несут ответственности за любое неправомерное использование приложения.
|
||||
- **[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
|
||||
|
||||
## Скриншоты
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
@@ -10,7 +10,7 @@
|
||||
<b>README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ve <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Belge</a>'sini ana dilinize çevirmemiz için yardımınıza ihtiyacımız var</b>
|
||||
</p>
|
||||
|
||||
Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -18,7 +18,7 @@ Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kul
|
||||
|
||||

|
||||
|
||||
RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın.
|
||||
RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](CONTRIBUTING-TR.md) belgesine göz atın.
|
||||
|
||||
[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B>
|
||||
</p>
|
||||
|
||||
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||

|
||||
|
||||
RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](docs/CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
|
||||
RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
|
||||
|
||||
[**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
|
||||
</p>
|
||||
|
||||
Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
</p>
|
||||
|
||||
> [!警告]
|
||||
> [!CAUTION]
|
||||
> **免责声明:** <br>
|
||||
> RustDesk 的开发人员不纵容或支持任何不道德或非法的软件使用行为。滥用行为,例如未经授权的访问、控制或侵犯隐私,严格违反我们的准则。作者对应用程序的任何滥用行为概不负责。
|
||||
|
||||
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
|
||||
7
docs/SECURITY-KR.md
Normal file
7
docs/SECURITY-KR.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 보안 정책
|
||||
|
||||
## 취약점 보고
|
||||
|
||||
저희는 프로젝트의 보안을 매우 중요하게 생각합니다. 모든 사용자가 발견한 취약점을 저희에게 보고할 것을 권장합니다. RustDesk 프로젝트에서 보안 취약점이 발견되면 info@rustdesk.com으로 이메일을 보내 책임감 있게 보고해 주시기 바랍니다.
|
||||
|
||||
현재로서는 버그 현상금 프로그램이 없습니다. 저희는 큰 문제를 해결하기 위해 노력하는 소규모 팀입니다. 전체 커뮤니티를 위한 안전한 응용 프로그램을 계속 구축할 수 있도록 취약점을 책임감 있게 신고해 주시기 바랍니다.
|
||||
@@ -1 +1 @@
|
||||
An open-source remote desktop application, the open source TeamViewer alternative.
|
||||
An open-source remote desktop application, the TeamViewer alternative
|
||||
|
||||
@@ -70,7 +70,7 @@ class InputService : AccessibilityService() {
|
||||
|
||||
private val logTag = "input service"
|
||||
private var leftIsDown = false
|
||||
private val touchPath = Path()
|
||||
private var touchPath = Path()
|
||||
private var stroke: GestureDescription.StrokeDescription? = null
|
||||
private var lastTouchGestureStartTime = 0L
|
||||
private var mouseX = 0
|
||||
@@ -278,7 +278,11 @@ class InputService : AccessibilityService() {
|
||||
}
|
||||
|
||||
private fun startGesture(x: Int, y: Int) {
|
||||
touchPath.reset()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
touchPath.reset()
|
||||
} else {
|
||||
touchPath = Path()
|
||||
}
|
||||
touchPath.moveTo(x.toFloat(), y.toFloat())
|
||||
lastTouchGestureStartTime = System.currentTimeMillis()
|
||||
lastX = x
|
||||
@@ -294,14 +298,31 @@ class InputService : AccessibilityService() {
|
||||
}
|
||||
try {
|
||||
if (stroke == null) {
|
||||
stroke = GestureDescription.StrokeDescription(
|
||||
touchPath,
|
||||
0,
|
||||
duration,
|
||||
willContinue
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
stroke = GestureDescription.StrokeDescription(
|
||||
touchPath,
|
||||
0,
|
||||
duration,
|
||||
willContinue
|
||||
)
|
||||
} else {
|
||||
stroke = GestureDescription.StrokeDescription(
|
||||
touchPath,
|
||||
0,
|
||||
duration
|
||||
)
|
||||
}
|
||||
} else {
|
||||
stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue)
|
||||
} else {
|
||||
stroke = null
|
||||
stroke = GestureDescription.StrokeDescription(
|
||||
touchPath,
|
||||
0,
|
||||
duration
|
||||
)
|
||||
}
|
||||
}
|
||||
stroke?.let {
|
||||
val builder = GestureDescription.Builder()
|
||||
@@ -316,19 +337,49 @@ class InputService : AccessibilityService() {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun continueGesture(x: Int, y: Int) {
|
||||
doDispatchGesture(x, y, true)
|
||||
touchPath.reset()
|
||||
touchPath.moveTo(x.toFloat(), y.toFloat())
|
||||
lastTouchGestureStartTime = System.currentTimeMillis()
|
||||
lastX = x
|
||||
lastY = y
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
doDispatchGesture(x, y, true)
|
||||
touchPath.reset()
|
||||
touchPath.moveTo(x.toFloat(), y.toFloat())
|
||||
lastTouchGestureStartTime = System.currentTimeMillis()
|
||||
lastX = x
|
||||
lastY = y
|
||||
} else {
|
||||
touchPath.lineTo(x.toFloat(), y.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun endGestureBelowO(x: Int, y: Int) {
|
||||
try {
|
||||
touchPath.lineTo(x.toFloat(), y.toFloat())
|
||||
var duration = System.currentTimeMillis() - lastTouchGestureStartTime
|
||||
if (duration <= 0) {
|
||||
duration = 1
|
||||
}
|
||||
val stroke = GestureDescription.StrokeDescription(
|
||||
touchPath,
|
||||
0,
|
||||
duration
|
||||
)
|
||||
val builder = GestureDescription.Builder()
|
||||
builder.addStroke(stroke)
|
||||
Log.d(logTag, "end gesture x:$x y:$y time:$duration")
|
||||
dispatchGesture(builder.build(), null, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(logTag, "endGesture error:$e")
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun endGesture(x: Int, y: Int) {
|
||||
doDispatchGesture(x, y, false)
|
||||
touchPath.reset()
|
||||
stroke = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
doDispatchGesture(x, y, false)
|
||||
touchPath.reset()
|
||||
stroke = null
|
||||
} else {
|
||||
endGestureBelowO(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
|
||||
@@ -316,7 +316,7 @@ class MainActivity : FlutterActivity() {
|
||||
codecObject.put("mime_type", mime_type)
|
||||
val caps = codec.getCapabilitiesForType(mime_type)
|
||||
if (codec.isEncoder) {
|
||||
// Encoder‘s max_height and max_width are interchangeable
|
||||
// Encoder's max_height and max_width are interchangeable
|
||||
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
@@ -122,9 +122,9 @@ class MainService : Service() {
|
||||
val authorized = jsonObject["authorized"] as Boolean
|
||||
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
||||
val type = if (isFileTransfer) {
|
||||
translate("File Connection")
|
||||
translate("Transfer file")
|
||||
} else {
|
||||
translate("Screen Connection")
|
||||
translate("Share screen")
|
||||
}
|
||||
if (authorized) {
|
||||
if (!isFileTransfer && !isStart) {
|
||||
|
||||
@@ -18,8 +18,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
|
||||
id "com.android.application" version "7.3.1" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
# no obfuscate, because no easy to check errors
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
cd -
|
||||
flutter build ipa --release
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||
|
||||
@@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart';
|
||||
import 'mobile/pages/file_manager_page.dart';
|
||||
import 'mobile/pages/remote_page.dart';
|
||||
import 'mobile/pages/view_camera_page.dart';
|
||||
import 'mobile/pages/terminal_page.dart';
|
||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
|
||||
import 'desktop/pages/view_camera_page.dart' as desktop_view_camera;
|
||||
@@ -99,6 +100,7 @@ enum DesktopType {
|
||||
remote,
|
||||
fileTransfer,
|
||||
viewCamera,
|
||||
terminal,
|
||||
cm,
|
||||
portForward,
|
||||
}
|
||||
@@ -1152,15 +1154,23 @@ Widget createDialogContent(String text) {
|
||||
|
||||
void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
|
||||
{bool? hasCancel,
|
||||
ReconnectHandle? reconnect,
|
||||
int? reconnectTimeout,
|
||||
VoidCallback? onSubmit,
|
||||
int? submitTimeout}) {
|
||||
dialogManager.dismissAll();
|
||||
List<Widget> buttons = [];
|
||||
bool hasOk = false;
|
||||
submit() {
|
||||
dialogManager.dismissAll();
|
||||
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
|
||||
closeConnection();
|
||||
if (onSubmit != null) {
|
||||
onSubmit.call();
|
||||
} else {
|
||||
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
|
||||
closeConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1176,7 +1186,18 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
|
||||
if (type != "connecting" && type != "success" && !type.contains("nook")) {
|
||||
hasOk = true;
|
||||
buttons.insert(0, dialogButton('OK', onPressed: submit));
|
||||
late final Widget btn;
|
||||
if (submitTimeout != null) {
|
||||
btn = _CountDownButton(
|
||||
text: 'OK',
|
||||
second: submitTimeout,
|
||||
onPressed: submit,
|
||||
submitOnTimeout: true,
|
||||
);
|
||||
} else {
|
||||
btn = dialogButton('OK', onPressed: submit);
|
||||
}
|
||||
buttons.insert(0, btn);
|
||||
}
|
||||
hasCancel ??= !type.contains("error") &&
|
||||
!type.contains("nocancel") &&
|
||||
@@ -1197,7 +1218,8 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
reconnectTimeout != null) {
|
||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||
final enabled = true.obs;
|
||||
final button = Obx(() => _ReconnectCountDownButton(
|
||||
final button = Obx(() => _CountDownButton(
|
||||
text: 'Reconnect',
|
||||
second: reconnectTimeout,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
@@ -1551,7 +1573,9 @@ bool option2bool(String option, String value) {
|
||||
|
||||
String bool2option(String option, bool b) {
|
||||
String res;
|
||||
if (option.startsWith('enable-')) {
|
||||
if (option.startsWith('enable-') &&
|
||||
option != kOptionEnableUdpPunch &&
|
||||
option != kOptionEnableIpv6Punch) {
|
||||
res = b ? defaultOptionYes : 'N';
|
||||
} else if (option.startsWith('allow-') ||
|
||||
option == kOptionStopService ||
|
||||
@@ -1559,7 +1583,9 @@ String bool2option(String option, bool b) {
|
||||
option == kOptionForceAlwaysRelay) {
|
||||
res = b ? 'Y' : defaultOptionNo;
|
||||
} else {
|
||||
assert(false);
|
||||
if (option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) {
|
||||
assert(false);
|
||||
}
|
||||
res = b ? 'Y' : 'N';
|
||||
}
|
||||
return res;
|
||||
@@ -2097,6 +2123,11 @@ enum UriLinkType {
|
||||
viewCamera,
|
||||
portForward,
|
||||
rdp,
|
||||
terminal,
|
||||
}
|
||||
|
||||
setEnvTerminalAdmin() {
|
||||
bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y');
|
||||
}
|
||||
|
||||
// uri link handler
|
||||
@@ -2161,6 +2192,17 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--terminal':
|
||||
type = UriLinkType.terminal;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--terminal-admin':
|
||||
setEnvTerminalAdmin();
|
||||
type = UriLinkType.terminal;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--password':
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
@@ -2210,6 +2252,12 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
case UriLinkType.terminal:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newTerminal(id!,
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -2227,7 +2275,9 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
"file-transfer",
|
||||
"view-camera",
|
||||
"port-forward",
|
||||
"rdp"
|
||||
"rdp",
|
||||
"terminal",
|
||||
"terminal-admin",
|
||||
];
|
||||
if (uri.authority.isEmpty &&
|
||||
uri.path.split('').every((char) => char == '/')) {
|
||||
@@ -2256,21 +2306,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
}
|
||||
}
|
||||
} else if (options.contains(uri.authority)) {
|
||||
final optionIndex = options.indexOf(uri.authority);
|
||||
command = '--${uri.authority}';
|
||||
if (uri.path.length > 1) {
|
||||
id = uri.path.substring(1);
|
||||
}
|
||||
if (isMobile && id != null) {
|
||||
if (optionIndex == 0 || optionIndex == 1) {
|
||||
connect(Get.context!, id);
|
||||
} else if (optionIndex == 2) {
|
||||
connect(Get.context!, id, isFileTransfer: true);
|
||||
} else if (optionIndex == 3) {
|
||||
connect(Get.context!, id, isViewCamera: true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else if (uri.authority.length > 2 &&
|
||||
(uri.path.length <= 1 ||
|
||||
(uri.path == '/r' || uri.path.startsWith('/r@')))) {
|
||||
@@ -2294,12 +2333,29 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
}
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
if (id != null) {
|
||||
final forceRelay = queryParameters["relay"] != null;
|
||||
connect(Get.context!, id, forceRelay: forceRelay);
|
||||
return null;
|
||||
if (isMobile && id != null) {
|
||||
final forceRelay = queryParameters["relay"] != null;
|
||||
final password = queryParameters["password"];
|
||||
|
||||
// Determine connection type based on command
|
||||
if (command == '--file-transfer') {
|
||||
connect(Get.context!, id,
|
||||
isFileTransfer: true, forceRelay: forceRelay, password: password);
|
||||
} else if (command == '--view-camera') {
|
||||
connect(Get.context!, id,
|
||||
isViewCamera: true, forceRelay: forceRelay, password: password);
|
||||
} else if (command == '--terminal') {
|
||||
connect(Get.context!, id,
|
||||
isTerminal: true, forceRelay: forceRelay, password: password);
|
||||
} else if (command == 'terminal-admin') {
|
||||
setEnvTerminalAdmin();
|
||||
connect(Get.context!, id,
|
||||
isTerminal: true, forceRelay: forceRelay, password: password);
|
||||
} else {
|
||||
// Default to remote desktop for '--connect', '--play', or direct connection
|
||||
connect(Get.context!, id, forceRelay: forceRelay, password: password);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> args = List.empty(growable: true);
|
||||
@@ -2321,6 +2377,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
connectMainDesktop(String id,
|
||||
{required bool isFileTransfer,
|
||||
required bool isViewCamera,
|
||||
required bool isTerminal,
|
||||
required bool isTcpTunneling,
|
||||
required bool isRDP,
|
||||
bool? forceRelay,
|
||||
@@ -2345,6 +2402,12 @@ connectMainDesktop(String id,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isTerminal) {
|
||||
await rustDeskWinManager.newTerminal(id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else {
|
||||
await rustDeskWinManager.newRemoteDesktop(id,
|
||||
password: password,
|
||||
@@ -2361,6 +2424,7 @@ connectMainDesktop(String id,
|
||||
connect(BuildContext context, String id,
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTerminal = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false,
|
||||
@@ -2383,7 +2447,7 @@ connect(BuildContext context, String id,
|
||||
id = id.replaceAll(' ', '');
|
||||
final oldId = id;
|
||||
id = await bind.mainHandleRelayId(id: id);
|
||||
final forceRelay2 = id != oldId || forceRelay;
|
||||
forceRelay = id != oldId || forceRelay;
|
||||
assert(!(isFileTransfer && isTcpTunneling && isRDP),
|
||||
"more than one connect type");
|
||||
|
||||
@@ -2393,17 +2457,19 @@ connect(BuildContext context, String id,
|
||||
id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay2,
|
||||
forceRelay: forceRelay,
|
||||
);
|
||||
} else {
|
||||
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
|
||||
'id': id,
|
||||
'isFileTransfer': isFileTransfer,
|
||||
'isViewCamera': isViewCamera,
|
||||
'isTerminal': isTerminal,
|
||||
'isTcpTunneling': isTcpTunneling,
|
||||
'isRDP': isRDP,
|
||||
'password': password,
|
||||
@@ -2437,7 +2503,10 @@ connect(BuildContext context, String id,
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => FileManagerPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2452,7 +2521,6 @@ connect(BuildContext context, String id,
|
||||
id: id,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
),
|
||||
),
|
||||
@@ -2462,10 +2530,25 @@ connect(BuildContext context, String id,
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ViewCameraPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (isTerminal) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => TerminalPage(
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (isWeb) {
|
||||
Navigator.push(
|
||||
@@ -2476,7 +2559,6 @@ connect(BuildContext context, String id,
|
||||
id: id,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
),
|
||||
),
|
||||
@@ -2486,7 +2568,10 @@ connect(BuildContext context, String id,
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => RemotePage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2858,6 +2943,7 @@ Future<bool> canBeBlocked() async {
|
||||
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
||||
}
|
||||
|
||||
// to-do: web not implemented
|
||||
Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
|
||||
if (use != null && !await use()) {
|
||||
block.value = false;
|
||||
@@ -3183,21 +3269,24 @@ parseParamScreenRect(Map<String, dynamic> params) {
|
||||
|
||||
get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2";
|
||||
|
||||
class _ReconnectCountDownButton extends StatefulWidget {
|
||||
_ReconnectCountDownButton({
|
||||
class _CountDownButton extends StatefulWidget {
|
||||
_CountDownButton({
|
||||
Key? key,
|
||||
required this.text,
|
||||
required this.second,
|
||||
required this.onPressed,
|
||||
this.submitOnTimeout = false,
|
||||
}) : super(key: key);
|
||||
final String text;
|
||||
final VoidCallback? onPressed;
|
||||
final int second;
|
||||
final bool submitOnTimeout;
|
||||
|
||||
@override
|
||||
State<_ReconnectCountDownButton> createState() =>
|
||||
_ReconnectCountDownButtonState();
|
||||
State<_CountDownButton> createState() => _CountDownButtonState();
|
||||
}
|
||||
|
||||
class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
|
||||
class _CountDownButtonState extends State<_CountDownButton> {
|
||||
late int _countdownSeconds = widget.second;
|
||||
|
||||
Timer? _timer;
|
||||
@@ -3218,6 +3307,9 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (_countdownSeconds <= 0) {
|
||||
timer.cancel();
|
||||
if (widget.submitOnTimeout) {
|
||||
widget.onPressed?.call();
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_countdownSeconds--;
|
||||
@@ -3229,7 +3321,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return dialogButton(
|
||||
'${translate('Reconnect')} (${_countdownSeconds}s)',
|
||||
'${translate(widget.text)} (${_countdownSeconds}s)',
|
||||
onPressed: widget.onPressed,
|
||||
isOutline: true,
|
||||
);
|
||||
@@ -3419,6 +3511,9 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
|
||||
}
|
||||
|
||||
Widget loadPowered(BuildContext context) {
|
||||
if (bind.mainGetBuildinOption(key: "hide-powered-by-me") == 'Y') {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
|
||||
@@ -27,6 +27,7 @@ class UserPayload {
|
||||
String name = '';
|
||||
String email = '';
|
||||
String note = '';
|
||||
String? verifier;
|
||||
UserStatus status;
|
||||
bool isAdmin = false;
|
||||
|
||||
@@ -34,6 +35,7 @@ class UserPayload {
|
||||
: name = json['name'] ?? '',
|
||||
email = json['email'] ?? '',
|
||||
note = json['note'] ?? '',
|
||||
verifier = json['verifier'],
|
||||
status = json['status'] == 0
|
||||
? UserStatus.kDisabled
|
||||
: json['status'] == -1
|
||||
|
||||
@@ -819,23 +819,33 @@ void enterPasswordDialog(
|
||||
}
|
||||
|
||||
void enterUserLoginDialog(
|
||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||
SessionID sessionId,
|
||||
OverlayDialogManager dialogManager,
|
||||
String osAccountDescTip,
|
||||
bool canRememberAccount) async {
|
||||
await _connectDialog(
|
||||
sessionId,
|
||||
dialogManager,
|
||||
osUsernameController: TextEditingController(),
|
||||
osPasswordController: TextEditingController(),
|
||||
osAccountDescTip: osAccountDescTip,
|
||||
canRememberAccount: canRememberAccount,
|
||||
);
|
||||
}
|
||||
|
||||
void enterUserLoginAndPasswordDialog(
|
||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||
SessionID sessionId,
|
||||
OverlayDialogManager dialogManager,
|
||||
String osAccountDescTip,
|
||||
bool canRememberAccount) async {
|
||||
await _connectDialog(
|
||||
sessionId,
|
||||
dialogManager,
|
||||
osUsernameController: TextEditingController(),
|
||||
osPasswordController: TextEditingController(),
|
||||
passwordController: TextEditingController(),
|
||||
osAccountDescTip: osAccountDescTip,
|
||||
canRememberAccount: canRememberAccount,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -845,17 +855,28 @@ _connectDialog(
|
||||
TextEditingController? osUsernameController,
|
||||
TextEditingController? osPasswordController,
|
||||
TextEditingController? passwordController,
|
||||
String? osAccountDescTip,
|
||||
bool canRememberAccount = true,
|
||||
}) async {
|
||||
final errUsername = ''.obs;
|
||||
var rememberPassword = false;
|
||||
if (passwordController != null) {
|
||||
rememberPassword =
|
||||
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
|
||||
}
|
||||
var rememberAccount = false;
|
||||
if (osUsernameController != null) {
|
||||
if (canRememberAccount && osUsernameController != null) {
|
||||
rememberAccount =
|
||||
await bind.sessionGetRemember(sessionId: sessionId) ?? false;
|
||||
}
|
||||
if (osUsernameController != null) {
|
||||
osUsernameController.addListener(() {
|
||||
if (errUsername.value.isNotEmpty) {
|
||||
errUsername.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close, context) {
|
||||
cancel() {
|
||||
@@ -864,6 +885,13 @@ _connectDialog(
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (osUsernameController != null) {
|
||||
if (osUsernameController.text.trim().isEmpty) {
|
||||
errUsername.value = translate('Empty Username');
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
final osUsername = osUsernameController?.text.trim() ?? '';
|
||||
final osPassword = osPasswordController?.text.trim() ?? '';
|
||||
final password = passwordController?.text.trim() ?? '';
|
||||
@@ -927,26 +955,39 @@ _connectDialog(
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
descWidget(translate('login_linux_tip')),
|
||||
if (osAccountDescTip != null) descWidget(translate(osAccountDescTip)),
|
||||
DialogTextField(
|
||||
title: translate(DialogTextField.kUsernameTitle),
|
||||
controller: osUsernameController,
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: null,
|
||||
),
|
||||
if (errUsername.value.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SelectableText(
|
||||
errUsername.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
).paddingOnly(left: 12, bottom: 2),
|
||||
),
|
||||
PasswordWidget(
|
||||
controller: osPasswordController,
|
||||
autoFocus: false,
|
||||
),
|
||||
rememberWidget(
|
||||
translate('remember_account_tip'),
|
||||
rememberAccount,
|
||||
(v) {
|
||||
if (v != null) {
|
||||
setState(() => rememberAccount = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (canRememberAccount)
|
||||
rememberWidget(
|
||||
translate('remember_account_tip'),
|
||||
rememberAccount,
|
||||
(v) {
|
||||
if (v != null) {
|
||||
setState(() => rememberAccount = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1136,7 +1177,7 @@ void showRequestElevationDialog(
|
||||
DialogTextField(
|
||||
controller: userController,
|
||||
title: translate('Username'),
|
||||
hintText: translate('eg: admin'),
|
||||
hintText: translate('elevation_username_tip'),
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: errUser.isEmpty ? null : errUser.value,
|
||||
),
|
||||
@@ -1623,6 +1664,28 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||
}
|
||||
|
||||
trackpadSpeedDialog(SessionID sessionId, FFI ffi) async {
|
||||
int initSpeed = ffi.inputModel.trackpadSpeed;
|
||||
final curSpeed = SimpleWrapper(initSpeed);
|
||||
final btnClose = dialogButton('Close', onPressed: () async {
|
||||
if (curSpeed.value <= kMaxTrackpadSpeed &&
|
||||
curSpeed.value >= kMinTrackpadSpeed &&
|
||||
curSpeed.value != initSpeed) {
|
||||
await bind.sessionSetTrackpadSpeed(
|
||||
sessionId: sessionId, value: curSpeed.value);
|
||||
await ffi.inputModel.updateTrackpadSpeed();
|
||||
}
|
||||
ffi.dialogManager.dismissAll();
|
||||
});
|
||||
msgBoxCommon(
|
||||
ffi.dialogManager,
|
||||
'Trackpad speed',
|
||||
TrackpadSpeedWidget(
|
||||
value: curSpeed,
|
||||
),
|
||||
[btnClose]);
|
||||
}
|
||||
|
||||
void deleteConfirmDialog(Function onSubmit, String title) async {
|
||||
gFFI.dialogManager.show(
|
||||
(setState, close, context) {
|
||||
|
||||
@@ -166,10 +166,13 @@ class _WidgetOPState extends State<WidgetOP> {
|
||||
final String stateMsg = resultMap['state_msg'];
|
||||
String failedMsg = resultMap['failed_msg'];
|
||||
final String? url = resultMap['url'];
|
||||
final bool urlLaunched = (resultMap['url_launched'] as bool?) ?? false;
|
||||
final authBody = resultMap['auth_body'];
|
||||
if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
|
||||
if (_url.isEmpty && url != null && url.isNotEmpty) {
|
||||
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
if (!urlLaunched) {
|
||||
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
_url = url;
|
||||
}
|
||||
if (authBody != null) {
|
||||
@@ -455,10 +458,14 @@ Future<bool?> loginDialog() async {
|
||||
resp.user, resp.secret, isEmailVerification);
|
||||
} else {
|
||||
setState(() => isInProgress = false);
|
||||
// Workaround for web, close the dialog first, then show the verification code dialog.
|
||||
// Otherwise, the text field will keep selecting the text and we can't input the code.
|
||||
// Not sure why this happens.
|
||||
if (isWeb && close != null) close(null);
|
||||
final res = await verificationCodeDialog(
|
||||
resp.user, resp.secret, isEmailVerification);
|
||||
if (res == true) {
|
||||
if (close != null) close(false);
|
||||
if (!isWeb && close != null) close(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +491,8 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool isTerminal = false,
|
||||
bool isTerminalRunAsAdmin = false,
|
||||
}) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
@@ -498,6 +500,9 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
if (isTerminalRunAsAdmin) {
|
||||
setEnvTerminalAdmin();
|
||||
}
|
||||
connectInPeerTab(
|
||||
context,
|
||||
peer,
|
||||
@@ -506,6 +511,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
isTerminal: isTerminal || isTerminalRunAsAdmin,
|
||||
);
|
||||
},
|
||||
padding: menuPadding,
|
||||
@@ -541,6 +547,24 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _terminalAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
'${translate('Terminal')} (beta)',
|
||||
isTerminal: true,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _terminalRunAsAdminAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
'${translate('Terminal (Run as administrator)')} (beta)',
|
||||
isTerminalRunAsAdmin: true,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
@@ -892,8 +916,13 @@ class RecentPeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
|
||||
if (peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_terminalRunAsAdminAction(context));
|
||||
}
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
@@ -952,7 +981,13 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
|
||||
if (peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_terminalRunAsAdminAction(context));
|
||||
}
|
||||
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
}
|
||||
@@ -1006,8 +1041,13 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
|
||||
if (peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_terminalRunAsAdminAction(context));
|
||||
}
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
@@ -1060,12 +1100,20 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
|
||||
if (peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_terminalRunAsAdminAction(context));
|
||||
}
|
||||
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
}
|
||||
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
||||
// menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (!isWeb) {
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
}
|
||||
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
@@ -1193,12 +1241,20 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
|
||||
if (peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_terminalRunAsAdminAction(context));
|
||||
}
|
||||
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
}
|
||||
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
||||
// menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (!isWeb) {
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
}
|
||||
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
@@ -1416,7 +1472,8 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false}) async {
|
||||
bool isRDP = false,
|
||||
bool isTerminal = false}) async {
|
||||
var password = '';
|
||||
bool isSharedPassword = false;
|
||||
if (tab == PeerTabIndex.ab) {
|
||||
@@ -1440,6 +1497,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTerminal: isTerminal,
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP);
|
||||
|
||||
@@ -501,6 +501,7 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
Widget build(BuildContext context) {
|
||||
final widget = super.build(context);
|
||||
bind.mainLoadLanPeers();
|
||||
bind.mainDiscover();
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,9 +111,13 @@ class _RawTouchGestureDetectorRegionState
|
||||
);
|
||||
}
|
||||
|
||||
bool isNotTouchBasedDevice() {
|
||||
return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
|
||||
}
|
||||
|
||||
onTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -126,7 +130,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
onTapUp(TapUpDetails d) async {
|
||||
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||
_lastTapDownDetails = null;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -142,7 +146,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onTap() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
@@ -153,7 +157,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onDoubleTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -163,7 +167,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onDoubleTap() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
||||
@@ -179,7 +183,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onLongPressDown(LongPressDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -198,7 +202,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onLongPressUp() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -208,7 +212,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
// for mobiles
|
||||
onLongPress() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!ffi.ffiModel.isPeerMobile) {
|
||||
@@ -228,7 +232,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async {
|
||||
if (!ffiModel.isPeerMobile || lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (!ffiModel.isPeerMobile || isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -241,7 +245,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onDoubleFinerTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
_doubleFinerTapPosition = d.localPosition;
|
||||
@@ -250,7 +254,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onDoubleFinerTap(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -266,7 +270,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onHoldDragStart(DragStartDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
@@ -275,7 +279,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onHoldDragUpdate(DragUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
@@ -284,7 +288,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onHoldDragEnd(DragEndDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
@@ -296,7 +300,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||
_lastTapDownDetails = null;
|
||||
lastDeviceKind = d.kind ?? lastDeviceKind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -342,7 +346,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onOneFingerPanUpdate(DragUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||
@@ -356,7 +360,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onOneFingerPanEnd(DragEndDetails d) async {
|
||||
_touchModePanStarted = false;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop || isWebDesktop) {
|
||||
@@ -370,13 +374,13 @@ class _RawTouchGestureDetectorRegionState
|
||||
// scale + pan event
|
||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||
_lastTapDownDetails = null;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
@@ -401,7 +405,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onTwoFingerScaleEnd(ScaleEndDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
|
||||
@@ -243,8 +243,99 @@ List<(String, String)> otherDefaultSettings() {
|
||||
(
|
||||
'Use all my displays for the remote session',
|
||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||
)
|
||||
),
|
||||
('Keep terminal sessions on disconnect', kOptionTerminalPersistent),
|
||||
];
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
class TrackpadSpeedWidget extends StatefulWidget {
|
||||
final SimpleWrapper<int> value;
|
||||
// If null, no debouncer will be applied.
|
||||
final Function(int)? onDebouncer;
|
||||
|
||||
TrackpadSpeedWidget({Key? key, required this.value, this.onDebouncer});
|
||||
|
||||
@override
|
||||
TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState();
|
||||
}
|
||||
|
||||
class TrackpadSpeedWidgetState extends State<TrackpadSpeedWidget> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
late final Debouncer<int> debouncerSpeed;
|
||||
|
||||
set value(int v) => widget.value.value = v;
|
||||
int get value => widget.value.value;
|
||||
|
||||
void updateValue(int newValue) {
|
||||
setState(() {
|
||||
value = newValue.clamp(kMinTrackpadSpeed, kMaxTrackpadSpeed);
|
||||
// Scale the trackpad speed value to a percentage for display purposes.
|
||||
_controller.text = value.toString();
|
||||
if (widget.onDebouncer != null) {
|
||||
debouncerSpeed.setValue(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
debouncerSpeed = Debouncer<int>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: widget.onDebouncer,
|
||||
initialValue: widget.value.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_controller.text.isEmpty) {
|
||||
_controller.text = value.toString();
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Slider(
|
||||
value: value.toDouble(),
|
||||
min: kMinTrackpadSpeed.toDouble(),
|
||||
max: kMaxTrackpadSpeed.toDouble(),
|
||||
divisions: ((kMaxTrackpadSpeed - kMinTrackpadSpeed) / 10).round(),
|
||||
onChanged: (double v) => updateValue(v.round()),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 56,
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (text) {
|
||||
int? v = int.tryParse(text);
|
||||
if (v != null) {
|
||||
updateValue(v);
|
||||
}
|
||||
},
|
||||
style: const TextStyle(fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
|
||||
),
|
||||
),
|
||||
).marginOnly(right: 8.0),
|
||||
Text(
|
||||
'%',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -15,7 +16,7 @@ bool isEditOsPassword = false;
|
||||
|
||||
class TTextMenu {
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback? onPressed;
|
||||
Widget? trailingIcon;
|
||||
bool divider;
|
||||
TTextMenu(
|
||||
@@ -153,36 +154,38 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
onPressed: () => ffi.cursorModel.reset()));
|
||||
}
|
||||
|
||||
// https://github.com/rustdesk/rustdesk/pull/9731
|
||||
// Does not work for connection established by "accept".
|
||||
connectWithToken(
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false}) {
|
||||
bool isTcpTunneling = false,
|
||||
bool isTerminal = false}) {
|
||||
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
connToken: connToken);
|
||||
}
|
||||
|
||||
// transferFile
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer file')),
|
||||
onPressed: () => connectWithToken(isFileTransfer: true)),
|
||||
);
|
||||
}
|
||||
// viewCamera
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('View camera')),
|
||||
onPressed: () => connectWithToken(isViewCamera: true)),
|
||||
);
|
||||
}
|
||||
// tcpTunneling
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text('${translate('Terminal')} (beta)'),
|
||||
onPressed: () => connectWithToken(isTerminal: true)),
|
||||
);
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP tunneling')),
|
||||
@@ -294,6 +297,41 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
),
|
||||
onPressed: () => ffi.recordingModel.toggle()));
|
||||
}
|
||||
|
||||
// to-do:
|
||||
// 1. Web desktop
|
||||
// 2. Mobile, copy the image to the clipboard
|
||||
if (isDesktop) {
|
||||
final isScreenshotSupported = bind.sessionGetCommonSync(
|
||||
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
||||
if ('true' == isScreenshotSupported) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(ffi.ffiModel.timerScreenshot != null
|
||||
? '${translate('Taking screenshot')} ...'
|
||||
: translate('Take screenshot')),
|
||||
onPressed: ffi.ffiModel.timerScreenshot != null
|
||||
? null
|
||||
: () {
|
||||
if (pi.currentDisplay == kAllDisplayValue) {
|
||||
msgBox(
|
||||
sessionId,
|
||||
'custom-nook-nocancel-hasclose-info',
|
||||
'Take screenshot',
|
||||
'screenshot-merged-screen-not-supported-tip',
|
||||
'',
|
||||
ffi.dialogManager);
|
||||
} else {
|
||||
bind.sessionTakeScreenshot(
|
||||
sessionId: sessionId, display: pi.currentDisplay);
|
||||
ffi.ffiModel.timerScreenshot =
|
||||
Timer(Duration(seconds: 30), () {
|
||||
ffi.ffiModel.timerScreenshot = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
// fingerprint
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
v.add(TTextMenu(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -27,7 +28,6 @@ const String kPlatformAdditionsAmyuniVirtualDisplays =
|
||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||
"supported_privacy_mode_impl";
|
||||
const String kPlatformAdditionsSupportViewCamera = "support_view_camera";
|
||||
|
||||
const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
@@ -47,6 +47,7 @@ const String kAppTypeDesktopRemote = "remote";
|
||||
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||
const String kAppTypeDesktopViewCamera = "view camera";
|
||||
const String kAppTypeDesktopPortForward = "port forward";
|
||||
const String kAppTypeDesktopTerminal = "terminal";
|
||||
|
||||
const String kWindowMainWindowOnTop = "main_window_on_top";
|
||||
const String kWindowGetWindowInfo = "get_window_info";
|
||||
@@ -62,6 +63,8 @@ const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
|
||||
const String kWindowEventNewFileTransfer = "new_file_transfer";
|
||||
const String kWindowEventNewViewCamera = "new_view_camera";
|
||||
const String kWindowEventNewPortForward = "new_port_forward";
|
||||
const String kWindowEventNewTerminal = "new_terminal";
|
||||
const String kWindowEventRestoreTerminalSessions = "restore_terminal_sessions";
|
||||
const String kWindowEventActiveSession = "active_session";
|
||||
const String kWindowEventActiveDisplaySession = "active_display_session";
|
||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||
@@ -103,6 +106,8 @@ const String kOptionEnableClipboard = "enable-clipboard";
|
||||
const String kOptionEnableFileTransfer = "enable-file-transfer";
|
||||
const String kOptionEnableAudio = "enable-audio";
|
||||
const String kOptionEnableCamera = "enable-camera";
|
||||
const String kOptionEnableTerminal = "enable-terminal";
|
||||
const String kOptionTerminalPersistent = "terminal-persistent";
|
||||
const String kOptionEnableTunnel = "enable-tunnel";
|
||||
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||
const String kOptionEnableBlockInput = "enable-block-input";
|
||||
@@ -110,6 +115,8 @@ const String kOptionAllowRemoteConfigModification =
|
||||
"allow-remote-config-modification";
|
||||
const String kOptionVerificationMethod = "verification-method";
|
||||
const String kOptionApproveMode = "approve-mode";
|
||||
const String kOptionAllowNumericOneTimePassword =
|
||||
"allow-numeric-one-time-password";
|
||||
const String kOptionCollapseToolbar = "collapse_toolbar";
|
||||
const String kOptionShowRemoteCursor = "show_remote_cursor";
|
||||
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
|
||||
@@ -139,16 +146,24 @@ const String kOptionCurrentAbName = "current-ab-name";
|
||||
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
||||
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
||||
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||
const String kOptionAllowAutoUpdate = "allow-auto-update";
|
||||
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||
const String kOptionStopService = "stop-service";
|
||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||
const String kOptionEnableUdpPunch = "enable-udp-punch";
|
||||
const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
|
||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||
|
||||
// network options
|
||||
const String kOptionAllowWebSocket = "allow-websocket";
|
||||
|
||||
// buildin opitons
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
|
||||
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
|
||||
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||
const String kOptionRemovePresetPasswordWarning =
|
||||
@@ -220,7 +235,14 @@ const double kDefaultQuality = 50;
|
||||
const double kMaxQuality = 100;
|
||||
const double kMaxMoreQuality = 2000;
|
||||
|
||||
const String kKeyPrinterIncommingJobAction = 'printer-incomming-job-action';
|
||||
// trackpad speed
|
||||
const String kKeyTrackpadSpeed = 'trackpad-speed';
|
||||
const int kMinTrackpadSpeed = 10;
|
||||
const int kDefaultTrackpadSpeed = 100;
|
||||
const int kMaxTrackpadSpeed = 1000;
|
||||
|
||||
// incomming (should be incoming) is kept, because change it will break the previous setting.
|
||||
const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action';
|
||||
const String kValuePrinterIncomingJobDismiss = 'dismiss';
|
||||
const String kValuePrinterIncomingJobDefault = '';
|
||||
const String kValuePrinterIncomingJobSelected = 'selected';
|
||||
@@ -316,6 +338,12 @@ const kRemoteImageQualityCustom = 'custom';
|
||||
|
||||
const kIgnoreDpi = true;
|
||||
|
||||
const Set<PointerDeviceKind> kTouchBasedDeviceKinds = {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
};
|
||||
|
||||
// ================================ mobile ================================
|
||||
|
||||
// Magic numbers, maybe need to avoid it or use a better way to get them.
|
||||
|
||||
@@ -327,10 +327,15 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
|
||||
/// Callback for the connect button.
|
||||
/// Connects to the selected peer.
|
||||
void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) {
|
||||
void onConnect(
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTerminal = false}) {
|
||||
var id = _idController.id;
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer, isViewCamera: isViewCamera);
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal);
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
@@ -527,22 +532,23 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Obx(() {
|
||||
var offset = Offset(0, 0);
|
||||
return InkWell(
|
||||
child: _menuOpen.value
|
||||
? Transform.rotate(
|
||||
angle: pi,
|
||||
child: Icon(IconFont.more, size: 14),
|
||||
)
|
||||
: Icon(IconFont.more, size: 14),
|
||||
onTapDown: (e) {
|
||||
offset = e.globalPosition;
|
||||
},
|
||||
onTap: () async {
|
||||
_menuOpen.value = true;
|
||||
final x = offset.dx;
|
||||
final y = offset.dy;
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
var offset = Offset(0, 0);
|
||||
return Obx(() => InkWell(
|
||||
child: _menuOpen.value
|
||||
? Transform.rotate(
|
||||
angle: pi,
|
||||
child: Icon(IconFont.more, size: 14),
|
||||
)
|
||||
: Icon(IconFont.more, size: 14),
|
||||
onTapDown: (e) {
|
||||
offset = e.globalPosition;
|
||||
},
|
||||
onTap: () async {
|
||||
_menuOpen.value = true;
|
||||
final x = offset.dx;
|
||||
final y = offset.dy;
|
||||
await mod_menu
|
||||
.showMenu(
|
||||
context: context,
|
||||
@@ -556,6 +562,10 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
'View camera',
|
||||
() => onConnect(isViewCamera: true)
|
||||
),
|
||||
(
|
||||
'${translate('Terminal')} (beta)',
|
||||
() => onConnect(isTerminal: true)
|
||||
),
|
||||
]
|
||||
.map((e) => MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
@@ -583,8 +593,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
_menuOpen.value = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/update_progress.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -22,7 +23,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:window_size/window_size.dart' as window_size;
|
||||
|
||||
import '../widgets/button.dart';
|
||||
|
||||
class DesktopHomePage extends StatefulWidget {
|
||||
@@ -433,13 +433,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
updateUrl.isNotEmpty &&
|
||||
!isCardClosed &&
|
||||
bind.mainUriPrefixSync().contains('rustdesk')) {
|
||||
final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled();
|
||||
String btnText = isToUpdate ? 'Click to update' : 'Click to download';
|
||||
GestureTapCallback onPressed = () async {
|
||||
final Uri url = Uri.parse('https://rustdesk.com/download');
|
||||
await launchUrl(url);
|
||||
};
|
||||
if (isToUpdate) {
|
||||
onPressed = () {
|
||||
handleUpdate(updateUrl);
|
||||
};
|
||||
}
|
||||
return buildInstallCard(
|
||||
"Status",
|
||||
"${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).",
|
||||
"Click to download", () async {
|
||||
final Uri url = Uri.parse('https://rustdesk.com/download');
|
||||
await launchUrl(url);
|
||||
}, closeButton: true);
|
||||
btnText,
|
||||
onPressed,
|
||||
closeButton: true);
|
||||
}
|
||||
if (systemError.isNotEmpty) {
|
||||
return buildInstallCard("", systemError, "", () {});
|
||||
@@ -776,6 +786,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
call.arguments['id'],
|
||||
isFileTransfer: call.arguments['isFileTransfer'],
|
||||
isViewCamera: call.arguments['isViewCamera'],
|
||||
isTerminal: call.arguments['isTerminal'],
|
||||
isTcpTunneling: call.arguments['isTcpTunneling'],
|
||||
isRDP: call.arguments['isRDP'],
|
||||
password: call.arguments['password'],
|
||||
|
||||
@@ -76,7 +76,9 @@ class DesktopSettingPage extends StatefulWidget {
|
||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
||||
SettingsTabKey.plugin,
|
||||
if (!bind.isDisableAccount()) SettingsTabKey.account,
|
||||
if (isWindows) SettingsTabKey.printer,
|
||||
if (isWindows &&
|
||||
bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y')
|
||||
SettingsTabKey.printer,
|
||||
SettingsTabKey.about,
|
||||
];
|
||||
|
||||
@@ -470,6 +472,8 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget other() {
|
||||
final showAutoUpdate =
|
||||
isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
|
||||
final children = <Widget>[
|
||||
if (!isWeb && !bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
@@ -523,12 +527,33 @@ class _GeneralState extends State<_General> {
|
||||
kOptionEnableCheckUpdate,
|
||||
isServer: false,
|
||||
),
|
||||
if (showAutoUpdate)
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Auto update',
|
||||
kOptionAllowAutoUpdate,
|
||||
isServer: true,
|
||||
),
|
||||
if (isWindows && !bind.isOutgoingOnly())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Capture screen using DirectX',
|
||||
kOptionDirectxCapture,
|
||||
)
|
||||
),
|
||||
if (!bind.isIncomingOnly()) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable UDP hole punching',
|
||||
kOptionEnableUdpPunch,
|
||||
isServer: false,
|
||||
),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable IPv6 P2P connection',
|
||||
kOptionEnableIpv6Punch,
|
||||
isServer: false,
|
||||
),
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||
@@ -986,6 +1011,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable camera', kOptionEnableCamera,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable terminal', kOptionEnableTerminal,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable TCP tunneling', kOptionEnableTunnel,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
@@ -1086,6 +1113,34 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
))
|
||||
.toList();
|
||||
|
||||
final isOptFixedNumOTP =
|
||||
isOptionFixed(kOptionAllowNumericOneTimePassword);
|
||||
final isNumOPTChangable = !isOptFixedNumOTP && tmpEnabled && !locked;
|
||||
final numericOneTimePassword = GestureDetector(
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: model.allowNumericOneTimePassword,
|
||||
onChanged: isNumOPTChangable
|
||||
? (bool? v) {
|
||||
model.switchAllowNumericOneTimePassword();
|
||||
}
|
||||
: null)
|
||||
.marginOnly(right: 5),
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate('Numeric one-time password'),
|
||||
style: TextStyle(
|
||||
color: disabledTextColor(context, isNumOPTChangable)),
|
||||
))
|
||||
],
|
||||
)),
|
||||
onTap: isNumOPTChangable
|
||||
? () => model.switchAllowNumericOneTimePassword()
|
||||
: null,
|
||||
).marginOnly(left: _kContentHSubMargin - 5);
|
||||
|
||||
final modeKeys = <String>[
|
||||
'password',
|
||||
'click',
|
||||
@@ -1122,6 +1177,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
],
|
||||
),
|
||||
enabled: tmpEnabled && !locked),
|
||||
numericOneTimePassword,
|
||||
if (usePassword) radios[1],
|
||||
if (usePassword)
|
||||
_SubButton('Set permanent password', setPasswordDialog,
|
||||
@@ -1466,11 +1522,69 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||
final hideProxy =
|
||||
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
final hideWebSocket = isWeb ||
|
||||
bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y';
|
||||
|
||||
if (hideServer && hideProxy) {
|
||||
if (hideServer && hideProxy && hideWebSocket) {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
// Helper function to create network setting ListTiles
|
||||
Widget listTile({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
VoidCallback? onTap,
|
||||
Widget? trailing,
|
||||
bool showTooltip = false,
|
||||
String tooltipMessage = '',
|
||||
}) {
|
||||
final titleWidget = showTooltip
|
||||
? Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 1000),
|
||||
message: translate(tooltipMessage),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
translate(title),
|
||||
style: TextStyle(fontSize: _kContentFontSize),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Icon(
|
||||
Icons.help_outline,
|
||||
size: 14,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.7),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
translate(title),
|
||||
style: TextStyle(fontSize: _kContentFontSize),
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: _accentColor),
|
||||
title: titleWidget,
|
||||
enabled: !locked,
|
||||
onTap: onTap,
|
||||
trailing: trailing,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
minLeadingWidth: 0,
|
||||
horizontalTitleGap: 10,
|
||||
);
|
||||
}
|
||||
|
||||
return _Card(
|
||||
title: 'Network',
|
||||
children: [
|
||||
@@ -1479,39 +1593,36 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!hideServer)
|
||||
ListTile(
|
||||
leading: Icon(Icons.dns_outlined, color: _accentColor),
|
||||
title: Text(
|
||||
translate('ID/Relay Server'),
|
||||
style: TextStyle(fontSize: _kContentFontSize),
|
||||
),
|
||||
enabled: !locked,
|
||||
listTile(
|
||||
icon: Icons.dns_outlined,
|
||||
title: 'ID/Relay Server',
|
||||
onTap: () => showServerSettings(gFFI.dialogManager),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
minLeadingWidth: 0,
|
||||
horizontalTitleGap: 10,
|
||||
),
|
||||
if (!hideServer && !hideProxy)
|
||||
if (!hideServer && (!hideProxy || !hideWebSocket))
|
||||
Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (!hideProxy)
|
||||
ListTile(
|
||||
leading:
|
||||
Icon(Icons.network_ping_outlined, color: _accentColor),
|
||||
title: Text(
|
||||
translate('Socks5/Http(s) Proxy'),
|
||||
style: TextStyle(fontSize: _kContentFontSize),
|
||||
),
|
||||
enabled: !locked,
|
||||
listTile(
|
||||
icon: Icons.network_ping_outlined,
|
||||
title: 'Socks5/Http(s) Proxy',
|
||||
onTap: changeSocks5Proxy,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
if (!hideProxy && !hideWebSocket)
|
||||
Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (!hideWebSocket)
|
||||
listTile(
|
||||
icon: Icons.web_asset_outlined,
|
||||
title: 'Use WebSocket',
|
||||
showTooltip: true,
|
||||
tooltipMessage: 'websocket_tip',
|
||||
trailing: Switch(
|
||||
value: mainGetBoolOptionSync(kOptionAllowWebSocket),
|
||||
onChanged: locked
|
||||
? null
|
||||
: (value) {
|
||||
mainSetBoolOption(kOptionAllowWebSocket, value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
minLeadingWidth: 0,
|
||||
horizontalTitleGap: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1537,6 +1648,7 @@ class _DisplayState extends State<_Display> {
|
||||
scrollStyle(context),
|
||||
imageQuality(context),
|
||||
codec(context),
|
||||
if (isDesktop) trackpadSpeed(context),
|
||||
if (!isWeb) privacyModeImpl(context),
|
||||
other(context),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin);
|
||||
@@ -1624,6 +1736,26 @@ class _DisplayState extends State<_Display> {
|
||||
]);
|
||||
}
|
||||
|
||||
Widget trackpadSpeed(BuildContext context) {
|
||||
final initSpeed = (int.tryParse(
|
||||
bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ??
|
||||
kDefaultTrackpadSpeed);
|
||||
final curSpeed = SimpleWrapper(initSpeed);
|
||||
void onDebouncer(int v) {
|
||||
bind.mainSetUserDefaultOption(
|
||||
key: kKeyTrackpadSpeed, value: v.toString());
|
||||
// It's better to notify all sessions that the default speed is changed.
|
||||
// But it may also be ok to take effect in the next connection.
|
||||
}
|
||||
|
||||
return _Card(title: 'Default trackpad speed', children: [
|
||||
TrackpadSpeedWidget(
|
||||
value: curSpeed,
|
||||
onDebouncer: onDebouncer,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget codec(BuildContext context) {
|
||||
onChanged(String value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
@@ -1908,7 +2040,7 @@ class __PrinterState extends State<_Printer> {
|
||||
final scrollController = ScrollController();
|
||||
return ListView(controller: scrollController, children: [
|
||||
outgoing(context),
|
||||
incomming(context),
|
||||
incoming(context),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin);
|
||||
}
|
||||
|
||||
@@ -1979,7 +2111,7 @@ class __PrinterState extends State<_Printer> {
|
||||
final installed = bind.mainIsInstalled();
|
||||
// `is-printer-installed` may fail, but it's rare case.
|
||||
// Add additional error message here if it's really needed.
|
||||
final driver_installed =
|
||||
final isPrinterInstalled =
|
||||
bind.mainGetCommonSync(key: 'is-printer-installed') == 'true';
|
||||
|
||||
final List<Widget> children = [];
|
||||
@@ -1988,22 +2120,22 @@ class __PrinterState extends State<_Printer> {
|
||||
} else {
|
||||
children.addAll([
|
||||
if (!installed) tipClientNotInstalled(),
|
||||
if (installed && !driver_installed) tipPrinterNotInstalled(),
|
||||
if (installed && driver_installed) tipReady()
|
||||
if (installed && !isPrinterInstalled) tipPrinterNotInstalled(),
|
||||
if (installed && isPrinterInstalled) tipReady()
|
||||
]);
|
||||
}
|
||||
return _Card(title: 'Outgoing Print Jobs', children: children);
|
||||
}
|
||||
|
||||
Widget incomming(BuildContext context) {
|
||||
Widget incoming(BuildContext context) {
|
||||
onRadioChanged(String value) async {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kKeyPrinterIncommingJobAction, value: value);
|
||||
key: kKeyPrinterIncomingJobAction, value: value);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
PrinterOptions printerOptions = PrinterOptions.load();
|
||||
return _Card(title: 'Incomming Print Jobs', children: [
|
||||
return _Card(title: 'Incoming Print Jobs', children: [
|
||||
_Radio(context,
|
||||
value: kValuePrinterIncomingJobDismiss,
|
||||
groupValue: printerOptions.action,
|
||||
|
||||
@@ -355,6 +355,7 @@ Widget buildConnectionCard(Client client) {
|
||||
_CmHeader(client: client),
|
||||
client.type_() == ClientType.file ||
|
||||
client.type_() == ClientType.portForward ||
|
||||
client.type_() == ClientType.terminal ||
|
||||
client.disconnected
|
||||
? Offstage()
|
||||
: _PrivilegeBoard(client: client),
|
||||
@@ -499,7 +500,36 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
"(${client.peerId})",
|
||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
).marginOnly(bottom: 10.0),
|
||||
),
|
||||
if (client.type_() == ClientType.terminal)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate("Terminal"),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (client.type_() == ClientType.file)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate("File Transfer"),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (client.type_() == ClientType.camera)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate("View Camera"),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (client.portForward.isNotEmpty)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
"Port Forward: ${client.portForward}",
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.0),
|
||||
FittedBox(
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
98
flutter/lib/desktop/pages/terminal_connection_manager.dart
Normal file
98
flutter/lib/desktop/pages/terminal_connection_manager.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../models/model.dart';
|
||||
|
||||
/// Manages terminal connections to ensure one FFI instance per peer
|
||||
class TerminalConnectionManager {
|
||||
static final Map<String, FFI> _connections = {};
|
||||
static final Map<String, int> _connectionRefCount = {};
|
||||
|
||||
// Track service IDs per peer
|
||||
static final Map<String, String> _serviceIds = {};
|
||||
|
||||
/// Get or create an FFI instance for a peer
|
||||
static FFI getConnection({
|
||||
required String peerId,
|
||||
required String? password,
|
||||
required bool? isSharedPassword,
|
||||
required bool? forceRelay,
|
||||
required String? connToken,
|
||||
}) {
|
||||
final existingFfi = _connections[peerId];
|
||||
if (existingFfi != null && !existingFfi.closed) {
|
||||
// Increment reference count
|
||||
_connectionRefCount[peerId] = (_connectionRefCount[peerId] ?? 0) + 1;
|
||||
debugPrint('[TerminalConnectionManager] Reusing existing connection for peer $peerId. Reference count: ${_connectionRefCount[peerId]}');
|
||||
return existingFfi;
|
||||
}
|
||||
|
||||
// Create new FFI instance for first terminal
|
||||
debugPrint('[TerminalConnectionManager] Creating new terminal connection for peer $peerId');
|
||||
final ffi = FFI(null);
|
||||
ffi.start(
|
||||
peerId,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay,
|
||||
connToken: connToken,
|
||||
isTerminal: true,
|
||||
);
|
||||
|
||||
_connections[peerId] = ffi;
|
||||
_connectionRefCount[peerId] = 1;
|
||||
|
||||
// Register the FFI instance with Get for dependency injection
|
||||
Get.put<FFI>(ffi, tag: 'terminal_$peerId');
|
||||
|
||||
debugPrint('[TerminalConnectionManager] New connection created. Total connections: ${_connections.length}');
|
||||
return ffi;
|
||||
}
|
||||
|
||||
/// Release a connection reference
|
||||
static void releaseConnection(String peerId) {
|
||||
final refCount = _connectionRefCount[peerId] ?? 0;
|
||||
debugPrint('[TerminalConnectionManager] Releasing connection for peer $peerId. Current ref count: $refCount');
|
||||
|
||||
if (refCount <= 1) {
|
||||
// Last reference, close the connection
|
||||
final ffi = _connections[peerId];
|
||||
if (ffi != null) {
|
||||
debugPrint('[TerminalConnectionManager] Closing connection for peer $peerId (last reference)');
|
||||
ffi.close();
|
||||
_connections.remove(peerId);
|
||||
_connectionRefCount.remove(peerId);
|
||||
Get.delete<FFI>(tag: 'terminal_$peerId');
|
||||
}
|
||||
} else {
|
||||
// Decrement reference count
|
||||
_connectionRefCount[peerId] = refCount - 1;
|
||||
debugPrint('[TerminalConnectionManager] Connection still in use. New ref count: ${_connectionRefCount[peerId]}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a connection exists for a peer
|
||||
static bool hasConnection(String peerId) {
|
||||
final ffi = _connections[peerId];
|
||||
return ffi != null && !ffi.closed;
|
||||
}
|
||||
|
||||
/// Get existing connection without creating new one
|
||||
static FFI? getExistingConnection(String peerId) {
|
||||
return _connections[peerId];
|
||||
}
|
||||
|
||||
/// Get connection count for debugging
|
||||
static int getConnectionCount() => _connections.length;
|
||||
|
||||
/// Get terminal count for a peer
|
||||
static int getTerminalCount(String peerId) => _connectionRefCount[peerId] ?? 0;
|
||||
|
||||
/// Get service ID for a peer
|
||||
static String? getServiceId(String peerId) => _serviceIds[peerId];
|
||||
|
||||
/// Set service ID for a peer
|
||||
static void setServiceId(String peerId, String serviceId) {
|
||||
_serviceIds[peerId] = serviceId;
|
||||
debugPrint('[TerminalConnectionManager] Service ID for $peerId: $serviceId');
|
||||
}
|
||||
}
|
||||
121
flutter/lib/desktop/pages/terminal_page.dart
Normal file
121
flutter/lib/desktop/pages/terminal_page.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
import 'terminal_connection_manager.dart';
|
||||
|
||||
class TerminalPage extends StatefulWidget {
|
||||
const TerminalPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.tabController,
|
||||
required this.isSharedPassword,
|
||||
required this.terminalId,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final DesktopTabController tabController;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final int terminalId;
|
||||
|
||||
@override
|
||||
State<TerminalPage> createState() => _TerminalPageState();
|
||||
}
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Use shared FFI instance from connection manager
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
peerId: widget.id,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
);
|
||||
|
||||
// Create terminal model with specific terminal ID
|
||||
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
||||
debugPrint(
|
||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
|
||||
// Check if this is a new connection or additional terminal
|
||||
// Note: When a connection exists, the ref count will be > 1 after this terminal is added
|
||||
final isExistingConnection = TerminalConnectionManager.hasConnection(widget.id) &&
|
||||
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
|
||||
|
||||
if (!isExistingConnection) {
|
||||
// First terminal - show loading dialog, wait for onReady
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
} else {
|
||||
// Additional terminal - connection already established
|
||||
// Open the terminal directly
|
||||
_terminalModel.openTerminal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Unregister terminal model from FFI
|
||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||
_terminalModel.dispose();
|
||||
// Release connection reference instead of closing directly
|
||||
TerminalConnectionManager.releaseConnection(widget.id);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 0.7,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
427
flutter/lib/desktop/pages/terminal_tab_page.dart
Normal file
427
flutter/lib/desktop/pages/terminal_tab_page.dart
Normal file
@@ -0,0 +1,427 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../models/platform_model.dart';
|
||||
import 'terminal_page.dart';
|
||||
import 'terminal_connection_manager.dart';
|
||||
import '../widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
import '../widgets/popup_menu.dart';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
|
||||
class TerminalTabPage extends StatefulWidget {
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
const TerminalTabPage({Key? key, required this.params}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TerminalTabPage> createState() => _TerminalTabPageState(params);
|
||||
}
|
||||
|
||||
class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
DesktopTabController get tabController => Get.find<DesktopTabController>();
|
||||
|
||||
static const IconData selectedIcon = Icons.terminal;
|
||||
static const IconData unselectedIcon = Icons.terminal_outlined;
|
||||
int _nextTerminalId = 1;
|
||||
|
||||
_TerminalTabPageState(Map<String, dynamic> params) {
|
||||
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
|
||||
tabController.onSelected = (id) {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
final terminalId = params['terminalId'] ?? _nextTerminalId++;
|
||||
tabController.add(_createTerminalTab(
|
||||
peerId: params['id'],
|
||||
terminalId: terminalId,
|
||||
password: params['password'],
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
));
|
||||
}
|
||||
|
||||
TabInfo _createTerminalTab({
|
||||
required String peerId,
|
||||
required int terminalId,
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
}) {
|
||||
final tabKey = '${peerId}_$terminalId';
|
||||
return TabInfo(
|
||||
key: tabKey,
|
||||
label: '$peerId #$terminalId',
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () async {
|
||||
// Close the terminal session first
|
||||
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
||||
if (ffi != null) {
|
||||
final terminalModel = ffi.terminalModels[terminalId];
|
||||
if (terminalModel != null) {
|
||||
await terminalModel.closeTerminal();
|
||||
}
|
||||
}
|
||||
// Then close the tab
|
||||
tabController.closeBy(tabKey);
|
||||
},
|
||||
page: TerminalPage(
|
||||
key: ValueKey(tabKey),
|
||||
id: peerId,
|
||||
terminalId: terminalId,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
tabController: tabController,
|
||||
forceRelay: forceRelay,
|
||||
connToken: connToken,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) {
|
||||
final List<MenuEntryBase<String>> menu = [];
|
||||
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
|
||||
|
||||
// New tab menu item
|
||||
menu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('New tab'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
_addNewTerminal(peerId);
|
||||
cancelFunc();
|
||||
// Also try to close any BotToast overlays
|
||||
BotToast.cleanAll();
|
||||
},
|
||||
padding: padding,
|
||||
));
|
||||
|
||||
menu.add(MenuEntryDivider());
|
||||
|
||||
menu.add(MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Keep terminal sessions on disconnect'),
|
||||
getter: () async {
|
||||
final ffi = Get.find<FFI>(tag: 'terminal_$peerId');
|
||||
return bind.sessionGetToggleOptionSync(
|
||||
sessionId: ffi.sessionId,
|
||||
arg: kOptionTerminalPersistent,
|
||||
);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
final ffi = Get.find<FFI>(tag: 'terminal_$peerId');
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId,
|
||||
value: kOptionTerminalPersistent,
|
||||
);
|
||||
},
|
||||
padding: padding,
|
||||
));
|
||||
|
||||
return mod_menu.PopupMenu<String>(
|
||||
items: menu
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: CustomPopupMenuTheme.commonColor,
|
||||
height: CustomPopupMenuTheme.height,
|
||||
dividerHeight: CustomPopupMenuTheme.dividerHeight,
|
||||
),
|
||||
))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Add keyboard shortcut handler
|
||||
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"[Remote Terminal] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
if (call.method == kWindowEventNewTerminal) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
windowOnTop(windowId());
|
||||
// Allow multiple terminals for the same connection
|
||||
final terminalId = args['terminalId'] ?? _nextTerminalId++;
|
||||
tabController.add(_createTerminalTab(
|
||||
peerId: id,
|
||||
terminalId: terminalId,
|
||||
password: args['password'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
));
|
||||
} else if (call.method == kWindowEventRestoreTerminalSessions) {
|
||||
_restoreSessions(call.arguments);
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventActiveSession) {
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
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)) {
|
||||
windowOnTop(windowId());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(WindowType.Terminal, windowId: windowId());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _restoreSessions(String arguments) async {
|
||||
Map<String, dynamic>? args;
|
||||
try {
|
||||
args = jsonDecode(arguments) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint("Error parsing JSON arguments in _restoreSessions: $e");
|
||||
return;
|
||||
}
|
||||
final persistentSessions =
|
||||
args['persistent_sessions'] as List<dynamic>? ?? [];
|
||||
final sortedSessions = persistentSessions.whereType<int>().toList()..sort();
|
||||
for (final terminalId in sortedSessions) {
|
||||
_addNewTerminalForCurrentPeer(terminalId: terminalId);
|
||||
// A delay is required to ensure the UI has sufficient time to update
|
||||
// before adding the next terminal. Without this delay, `_TerminalPageState::dispose()`
|
||||
// may be called prematurely while the tab widget is still in the tab controller.
|
||||
// This behavior is likely due to a race condition between the UI rendering lifecycle
|
||||
// and the addition of new tabs. Attempts to use `_TerminalPageState::addPostFrameCallback()`
|
||||
// to wait for the previous page to be ready were unsuccessful, as the observed call sequence is:
|
||||
// `initState() 2 -> dispose() 2 -> postFrameCallback() 2`, followed by `initState() 3`.
|
||||
// The `Future.delayed` approach mitigates this issue by introducing a buffer period,
|
||||
// allowing the UI to stabilize before proceeding.
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
|
||||
bool _handleKeyEvent(KeyEvent event) {
|
||||
if (event is KeyDownEvent) {
|
||||
// Use Cmd+T on macOS, Ctrl+Shift+T on other platforms
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyT) {
|
||||
if (isMacOS &&
|
||||
HardwareKeyboard.instance.isMetaPressed &&
|
||||
!HardwareKeyboard.instance.isShiftPressed) {
|
||||
// macOS: Cmd+T (standard for new tab)
|
||||
_addNewTerminalForCurrentPeer();
|
||||
return true;
|
||||
} else if (!isMacOS &&
|
||||
HardwareKeyboard.instance.isControlPressed &&
|
||||
HardwareKeyboard.instance.isShiftPressed) {
|
||||
// Other platforms: Ctrl+Shift+T (to avoid conflict with Ctrl+T in terminal)
|
||||
_addNewTerminalForCurrentPeer();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use Cmd+W on macOS, Ctrl+Shift+W on other platforms
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyW) {
|
||||
if (isMacOS &&
|
||||
HardwareKeyboard.instance.isMetaPressed &&
|
||||
!HardwareKeyboard.instance.isShiftPressed) {
|
||||
// macOS: Cmd+W (standard for close tab)
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
if (tabController.state.value.tabs.length > 1) {
|
||||
tabController.closeBy(currentTab.key);
|
||||
return true;
|
||||
}
|
||||
} else if (!isMacOS &&
|
||||
HardwareKeyboard.instance.isControlPressed &&
|
||||
HardwareKeyboard.instance.isShiftPressed) {
|
||||
// Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete)
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
if (tabController.state.value.tabs.length > 1) {
|
||||
tabController.closeBy(currentTab.key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use Alt+Left/Right for tab navigation (avoids conflicts)
|
||||
if (HardwareKeyboard.instance.isAltPressed) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
// Previous tab
|
||||
final currentIndex = tabController.state.value.selected;
|
||||
if (currentIndex > 0) {
|
||||
tabController.jumpTo(currentIndex - 1);
|
||||
}
|
||||
return true;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
// Next tab
|
||||
final currentIndex = tabController.state.value.selected;
|
||||
if (currentIndex < tabController.length - 1) {
|
||||
tabController.jumpTo(currentIndex + 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Cmd/Ctrl + Number (switch to specific tab)
|
||||
final numberKeys = [
|
||||
LogicalKeyboardKey.digit1,
|
||||
LogicalKeyboardKey.digit2,
|
||||
LogicalKeyboardKey.digit3,
|
||||
LogicalKeyboardKey.digit4,
|
||||
LogicalKeyboardKey.digit5,
|
||||
LogicalKeyboardKey.digit6,
|
||||
LogicalKeyboardKey.digit7,
|
||||
LogicalKeyboardKey.digit8,
|
||||
LogicalKeyboardKey.digit9,
|
||||
];
|
||||
|
||||
for (int i = 0; i < numberKeys.length; i++) {
|
||||
if (event.logicalKey == numberKeys[i] &&
|
||||
((isMacOS && HardwareKeyboard.instance.isMetaPressed) ||
|
||||
(!isMacOS && HardwareKeyboard.instance.isControlPressed))) {
|
||||
if (i < tabController.length) {
|
||||
tabController.jumpTo(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _addNewTerminal(String peerId, {int? terminalId}) {
|
||||
// Find first tab for this peer to get connection parameters
|
||||
final firstTab = tabController.state.value.tabs.firstWhere(
|
||||
(tab) => tab.key.startsWith('$peerId\_'),
|
||||
);
|
||||
if (firstTab.page is TerminalPage) {
|
||||
final page = firstTab.page as TerminalPage;
|
||||
final newTerminalId = terminalId ?? _nextTerminalId++;
|
||||
if (terminalId != null && terminalId >= _nextTerminalId) {
|
||||
_nextTerminalId = terminalId + 1;
|
||||
}
|
||||
tabController.add(_createTerminalTab(
|
||||
peerId: peerId,
|
||||
terminalId: newTerminalId,
|
||||
password: page.password,
|
||||
isSharedPassword: page.isSharedPassword,
|
||||
forceRelay: page.forceRelay,
|
||||
connToken: page.connToken,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: _buildAddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabMenuBuilder: (key) {
|
||||
// Extract peerId from tab key (format: "peerId_terminalId")
|
||||
final parts = key.split('_');
|
||||
if (parts.isEmpty) return Container();
|
||||
final peerId = parts[0];
|
||||
return _tabMenuBuilder(peerId, () {});
|
||||
},
|
||||
));
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: workaroundWindowBorder(
|
||||
context,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
);
|
||||
}
|
||||
|
||||
void onRemoveId(String id) {
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
WindowController.fromWindowId(windowId()).close();
|
||||
}
|
||||
}
|
||||
|
||||
int windowId() {
|
||||
return widget.params["windowId"];
|
||||
}
|
||||
|
||||
Widget _buildAddButton() {
|
||||
return ActionIcon(
|
||||
message: 'New tab',
|
||||
icon: IconFont.add,
|
||||
onTap: () {
|
||||
_addNewTerminalForCurrentPeer();
|
||||
},
|
||||
isClose: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.state.value.tabs.length;
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
} else {
|
||||
final bool res;
|
||||
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||
res = true;
|
||||
} else {
|
||||
res = await closeConfirmDialog();
|
||||
}
|
||||
if (res) {
|
||||
tabController.clear();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,8 +515,6 @@ class ImagePaint extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ImagePaintState extends State<ImagePaint> {
|
||||
bool _lastRemoteCursorMoved = false;
|
||||
|
||||
String get id => widget.id;
|
||||
RxBool get cursorOverImage => widget.cursorOverImage;
|
||||
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
||||
|
||||
27
flutter/lib/desktop/screen/desktop_terminal_screen.dart
Normal file
27
flutter/lib/desktop/screen/desktop_terminal_screen.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:flutter_hbb/desktop/pages/terminal_tab_page.dart';
|
||||
|
||||
class DesktopTerminalScreen extends StatelessWidget {
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
const DesktopTerminalScreen({Key? key, required this.params})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: TerminalTabPage(
|
||||
params: params,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -1594,10 +1595,28 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
viewMode(),
|
||||
Divider(),
|
||||
...toolbarToggles(),
|
||||
...mouseSpeed(),
|
||||
...mobileActions(),
|
||||
]);
|
||||
}
|
||||
|
||||
mouseSpeed() {
|
||||
final speedWidgets = [];
|
||||
final sessionId = ffi.sessionId;
|
||||
if (isDesktop) {
|
||||
if (ffi.ffiModel.keyboard) {
|
||||
final enabled = !ffi.ffiModel.viewOnly;
|
||||
final trackpad = MenuButton(
|
||||
child: Text(translate('Trackpad speed')).paddingOnly(left: 26.0),
|
||||
onPressed: enabled ? () => trackpadSpeedDialog(sessionId, ffi) : null,
|
||||
ffi: ffi,
|
||||
);
|
||||
speedWidgets.add(trackpad);
|
||||
}
|
||||
}
|
||||
return speedWidgets;
|
||||
}
|
||||
|
||||
keyboardMode() {
|
||||
return futureBuilder(future: () async {
|
||||
return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
|
||||
|
||||
@@ -54,6 +54,7 @@ enum DesktopTabType {
|
||||
fileTransfer,
|
||||
viewCamera,
|
||||
portForward,
|
||||
terminal,
|
||||
install,
|
||||
}
|
||||
|
||||
@@ -651,7 +652,9 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
controller.state.value.scrollController;
|
||||
if (!sc.canScroll) return;
|
||||
_scrollDebounce.call(() {
|
||||
sc.animateTo(sc.offset + e.scrollDelta.dy,
|
||||
double adjust = 2.5;
|
||||
sc.animateTo(
|
||||
sc.offset + e.scrollDelta.dy * adjust,
|
||||
duration: Duration(milliseconds: 200),
|
||||
curve: Curves.ease);
|
||||
});
|
||||
|
||||
234
flutter/lib/desktop/widgets/update_progress.dart
Normal file
234
flutter/lib/desktop/widgets/update_progress.dart
Normal file
@@ -0,0 +1,234 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
void handleUpdate(String releasePageUrl) {
|
||||
String downloadUrl = releasePageUrl.replaceAll('tag', 'download');
|
||||
String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1);
|
||||
final String downloadFile =
|
||||
bind.mainGetCommonSync(key: 'download-file-$version');
|
||||
if (downloadFile.startsWith('error:')) {
|
||||
final error = downloadFile.replaceFirst('error:', '');
|
||||
msgBox(gFFI.sessionId, 'custom-nocancel-nook-hasclose', 'Error', error,
|
||||
releasePageUrl, gFFI.dialogManager);
|
||||
return;
|
||||
}
|
||||
downloadUrl = '$downloadUrl/$downloadFile';
|
||||
|
||||
SimpleWrapper downloadId = SimpleWrapper('');
|
||||
SimpleWrapper<VoidCallback> onCanceled = SimpleWrapper(() {});
|
||||
gFFI.dialogManager.dismissAll();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Downloading {$appName}')),
|
||||
content:
|
||||
UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled)
|
||||
.marginSymmetric(horizontal: 8)
|
||||
.paddingOnly(top: 12),
|
||||
actions: [
|
||||
dialogButton(translate('Cancel'), onPressed: () async {
|
||||
onCanceled.value();
|
||||
await bind.mainSetCommon(
|
||||
key: 'cancel-downloader', value: downloadId.value);
|
||||
// Wait for the downloader to be removed.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
final isCanceled = 'error:Downloader not found' ==
|
||||
await bind.mainGetCommon(
|
||||
key: 'download-data-${downloadId.value}');
|
||||
if (isCanceled) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
close();
|
||||
}, isOutline: true),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
class UpdateProgress extends StatefulWidget {
|
||||
final String releasePageUrl;
|
||||
final String downloadUrl;
|
||||
final SimpleWrapper downloadId;
|
||||
final SimpleWrapper onCanceled;
|
||||
UpdateProgress(
|
||||
this.releasePageUrl, this.downloadUrl, this.downloadId, this.onCanceled,
|
||||
{Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<UpdateProgress> createState() => UpdateProgressState();
|
||||
}
|
||||
|
||||
class UpdateProgressState extends State<UpdateProgress> {
|
||||
Timer? _timer;
|
||||
int? _totalSize;
|
||||
int _downloadedSize = 0;
|
||||
int _getDataFailedCount = 0;
|
||||
final String _eventKeyDownloadNewVersion = 'download-new-version';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.onCanceled.value = () {
|
||||
cancelQueryTimer();
|
||||
};
|
||||
platformFFI.registerEventHandler(_eventKeyDownloadNewVersion,
|
||||
_eventKeyDownloadNewVersion, handleDownloadNewVersion,
|
||||
replace: true);
|
||||
bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cancelQueryTimer();
|
||||
platformFFI.unregisterEventHandler(
|
||||
_eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void cancelQueryTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
Future<void> handleDownloadNewVersion(Map<String, dynamic> evt) async {
|
||||
if (evt.containsKey('id')) {
|
||||
widget.downloadId.value = evt['id'] as String;
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 300), (timer) {
|
||||
_updateDownloadData();
|
||||
});
|
||||
} else {
|
||||
if (evt.containsKey('error')) {
|
||||
_onError(evt['error'] as String);
|
||||
} else {
|
||||
// unreachable
|
||||
_onError('$evt');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onError(String error) {
|
||||
cancelQueryTimer();
|
||||
|
||||
debugPrint('Download new version error: $error');
|
||||
final msgBoxType = 'custom-nocancel-nook-hasclose';
|
||||
final msgBoxTitle = 'Error';
|
||||
final msgBoxText = 'download-new-version-failed-tip';
|
||||
final dialogManager = gFFI.dialogManager;
|
||||
|
||||
close() {
|
||||
dialogManager.dismissAll();
|
||||
}
|
||||
|
||||
jumplink() {
|
||||
launchUrl(Uri.parse(widget.releasePageUrl));
|
||||
dialogManager.dismissAll();
|
||||
}
|
||||
|
||||
retry() {
|
||||
dialogManager.dismissAll();
|
||||
handleUpdate(widget.releasePageUrl);
|
||||
}
|
||||
|
||||
final List<Widget> buttons = [
|
||||
dialogButton('Download', onPressed: jumplink),
|
||||
dialogButton('Retry', onPressed: retry),
|
||||
dialogButton('Close', onPressed: close),
|
||||
];
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show(
|
||||
(setState, close, context) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: SelectionArea(
|
||||
child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)),
|
||||
actions: buttons,
|
||||
),
|
||||
tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle',
|
||||
);
|
||||
}
|
||||
|
||||
void _updateDownloadData() {
|
||||
String err = '';
|
||||
String downloadData =
|
||||
bind.mainGetCommonSync(key: 'download-data-${widget.downloadId.value}');
|
||||
if (downloadData.startsWith('error:')) {
|
||||
err = downloadData.substring('error:'.length);
|
||||
} else {
|
||||
try {
|
||||
jsonDecode(downloadData).forEach((key, value) {
|
||||
if (key == 'total_size') {
|
||||
if (value != null && value is int) {
|
||||
_totalSize = value;
|
||||
}
|
||||
} else if (key == 'downloaded_size') {
|
||||
_downloadedSize = value as int;
|
||||
} else if (key == 'error') {
|
||||
if (value != null) {
|
||||
err = value.toString();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
_getDataFailedCount += 1;
|
||||
debugPrint(
|
||||
'Failed to get download data ${widget.downloadUrl}, error $e');
|
||||
if (_getDataFailedCount > 3) {
|
||||
err = e.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (err != '') {
|
||||
_onError(err);
|
||||
} else {
|
||||
if (_totalSize != null && _downloadedSize >= _totalSize!) {
|
||||
cancelQueryTimer();
|
||||
bind.mainSetCommon(
|
||||
key: 'remove-downloader', value: widget.downloadId.value);
|
||||
if (_totalSize == 0) {
|
||||
_onError('The download file size is 0.');
|
||||
} else {
|
||||
setState(() {});
|
||||
msgBox(
|
||||
gFFI.sessionId,
|
||||
'custom-nocancel',
|
||||
'{$appName} Update',
|
||||
'{$appName}-to-update-tip',
|
||||
'',
|
||||
gFFI.dialogManager,
|
||||
onSubmit: () {
|
||||
debugPrint('Downloaded, update to new version now');
|
||||
bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl);
|
||||
},
|
||||
submitTimeout: 5,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return onDownloading(context);
|
||||
}
|
||||
|
||||
Widget onDownloading(BuildContext context) {
|
||||
final value = _totalSize == null
|
||||
? 0.0
|
||||
: (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!);
|
||||
return LinearProgressIndicator(
|
||||
value: value,
|
||||
minHeight: 20,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
backgroundColor: Colors.grey[300],
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_terminal_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
@@ -91,6 +92,12 @@ Future<void> main(List<String> args) async {
|
||||
kAppTypeDesktopPortForward,
|
||||
);
|
||||
break;
|
||||
case WindowType.Terminal:
|
||||
desktopType = DesktopType.terminal;
|
||||
runMultiWindow(
|
||||
argument,
|
||||
kAppTypeDesktopTerminal,
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -211,6 +218,11 @@ void runMultiWindow(
|
||||
params: argument,
|
||||
);
|
||||
break;
|
||||
case kAppTypeDesktopTerminal:
|
||||
widget = DesktopTerminalScreen(
|
||||
params: argument,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// no such appType
|
||||
exit(0);
|
||||
@@ -257,6 +269,9 @@ void runMultiWindow(
|
||||
case kAppTypeDesktopPortForward:
|
||||
await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
|
||||
break;
|
||||
case kAppTypeDesktopTerminal:
|
||||
await restoreWindowPosition(WindowType.Terminal, windowId: kWindowId!);
|
||||
break;
|
||||
default:
|
||||
// no such appType
|
||||
exit(0);
|
||||
|
||||
@@ -12,11 +12,12 @@ import '../../common/widgets/dialog.dart';
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
FileManagerPage(
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
@@ -74,7 +75,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
gFFI.start(widget.id,
|
||||
isFileTransfer: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword);
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
|
||||
@@ -205,13 +205,13 @@ class WebHomePage extends StatelessWidget {
|
||||
}
|
||||
bool isFileTransfer = false;
|
||||
bool isViewCamera = false;
|
||||
bool isTerminal = false;
|
||||
String? id;
|
||||
String? password;
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--connect':
|
||||
case '--play':
|
||||
isFileTransfer = false;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
@@ -225,6 +225,17 @@ class WebHomePage extends StatelessWidget {
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--terminal':
|
||||
isTerminal = true;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--terminal-admin':
|
||||
setEnvTerminalAdmin();
|
||||
isTerminal = true;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--password':
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
@@ -234,7 +245,11 @@ class WebHomePage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, password: password);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
password: password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,13 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
|
||||
}
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
|
||||
: super(key: key);
|
||||
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
|
||||
@override
|
||||
State<RemotePage> createState() => _RemotePageState(id);
|
||||
@@ -89,6 +90,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
widget.id,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
@@ -695,9 +697,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
);
|
||||
if (index != null) {
|
||||
if (index < mobileActionMenus.length) {
|
||||
mobileActionMenus[index].onPressed.call();
|
||||
mobileActionMenus[index].onPressed?.call();
|
||||
} else if (index < mobileActionMenus.length + more.length) {
|
||||
menus[index - mobileActionMenus.length].onPressed.call();
|
||||
menus[index - mobileActionMenus.length].onPressed?.call();
|
||||
}
|
||||
}
|
||||
}();
|
||||
@@ -770,7 +772,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
elevation: 8,
|
||||
);
|
||||
if (index != null && index < menus.length) {
|
||||
menus[index].onPressed.call();
|
||||
menus[index].onPressed?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1267,7 +1269,7 @@ void showOptions(
|
||||
title: resolution.child,
|
||||
onTap: () {
|
||||
close();
|
||||
resolution.onPressed();
|
||||
resolution.onPressed?.call();
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -1279,7 +1281,7 @@ void showOptions(
|
||||
title: virtualDisplayMenu.child,
|
||||
onTap: () {
|
||||
close();
|
||||
virtualDisplayMenu.onPressed();
|
||||
virtualDisplayMenu.onPressed?.call();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import 'home_page.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget implements PageShape {
|
||||
@override
|
||||
final title = translate("Share Screen");
|
||||
final title = translate("Share screen");
|
||||
|
||||
@override
|
||||
final icon = const Icon(Icons.mobile_screen_share);
|
||||
@@ -56,6 +56,10 @@ class _DropDownAction extends StatelessWidget {
|
||||
final verificationMethod = gFFI.serverModel.verificationMethod;
|
||||
final showPasswordOption = approveMode != 'click';
|
||||
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
|
||||
final isNumericOneTimePasswordFixed =
|
||||
isOptionFixed(kOptionAllowNumericOneTimePassword);
|
||||
final isAllowNumericOneTimePassword =
|
||||
gFFI.serverModel.allowNumericOneTimePassword;
|
||||
return [
|
||||
PopupMenuItem(
|
||||
enabled: gFFI.serverModel.connectStatus > 0,
|
||||
@@ -94,6 +98,14 @@ class _DropDownAction extends StatelessWidget {
|
||||
value: "setTemporaryPasswordLength",
|
||||
child: Text(translate("One-time password length")),
|
||||
),
|
||||
if (showPasswordOption &&
|
||||
verificationMethod != kUsePermanentPassword)
|
||||
PopupMenuItem(
|
||||
value: "allowNumericOneTimePassword",
|
||||
child: listTile(translate("Numeric one-time password"),
|
||||
isAllowNumericOneTimePassword),
|
||||
enabled: !isNumericOneTimePasswordFixed,
|
||||
),
|
||||
if (showPasswordOption) const PopupMenuDivider(),
|
||||
if (showPasswordOption)
|
||||
PopupMenuItem(
|
||||
@@ -124,6 +136,9 @@ class _DropDownAction extends StatelessWidget {
|
||||
setPasswordDialog();
|
||||
} else if (value == "setTemporaryPasswordLength") {
|
||||
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
||||
} else if (value == "allowNumericOneTimePassword") {
|
||||
gFFI.serverModel.switchAllowNumericOneTimePassword();
|
||||
gFFI.serverModel.updatePasswordModel();
|
||||
} else if (value == kUsePermanentPassword ||
|
||||
value == kUseTemporaryPassword ||
|
||||
value == kUseBothPasswords) {
|
||||
@@ -634,8 +649,8 @@ class ConnectionManager extends StatelessWidget {
|
||||
children: serverModel.clients
|
||||
.map((client) => PaddingCard(
|
||||
title: translate(client.isFileTransfer
|
||||
? "File Connection"
|
||||
: "Screen Connection"),
|
||||
? "Transfer file"
|
||||
: "Share screen"),
|
||||
titleIcon: client.isFileTransfer
|
||||
? Icon(Icons.folder_outlined)
|
||||
: Icon(Icons.mobile_screen_share),
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
@@ -80,6 +79,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _enableDirectIPAccess = false;
|
||||
var _enableRecordSession = false;
|
||||
var _enableHardwareCodec = false;
|
||||
var _allowWebSocket = false;
|
||||
var _autoRecordIncomingSession = false;
|
||||
var _autoRecordOutgoingSession = false;
|
||||
var _allowAutoDisconnect = false;
|
||||
@@ -91,7 +91,10 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _hideServer = false;
|
||||
var _hideProxy = false;
|
||||
var _hideNetwork = false;
|
||||
var _hideWebSocket = false;
|
||||
var _enableTrustedDevices = false;
|
||||
var _enableUdpPunch = false;
|
||||
var _enableIpv6Punch = false;
|
||||
|
||||
_SettingsState() {
|
||||
_enableAbr = option2bool(
|
||||
@@ -105,6 +108,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
bind.mainGetOptionSync(key: kOptionEnableRecordSession));
|
||||
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
||||
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||
_allowWebSocket = mainGetBoolOptionSync(kOptionAllowWebSocket);
|
||||
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
|
||||
@@ -120,7 +124,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
_hideNetwork =
|
||||
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
||||
_hideWebSocket =
|
||||
bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y' ||
|
||||
isWeb;
|
||||
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
||||
_enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
||||
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -243,7 +252,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
final incommingOnly = bind.isIncomingOnly();
|
||||
final incomingOnly = bind.isIncomingOnly();
|
||||
final customClientSection = CustomSettingsSection(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -369,7 +378,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text('${translate('Adaptive bitrate')} (beta)'),
|
||||
title: Text(translate('Adaptive bitrate')),
|
||||
initialValue: _enableAbr,
|
||||
onToggle: isOptionFixed(kOptionEnableAbr)
|
||||
? null
|
||||
@@ -531,7 +540,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
enhancementsTiles.add(SettingsTile.switchTile(
|
||||
initialValue: _enableStartOnBoot,
|
||||
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text("${translate('Start on boot')} (beta)"),
|
||||
Text(translate('Start on boot')),
|
||||
Text(
|
||||
'* ${translate('Start the screen sharing service on boot, requires special permissions')}',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
@@ -667,6 +676,47 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
onPressed: (context) {
|
||||
changeSocks5Proxy();
|
||||
}),
|
||||
if (!disabledSettings && !_hideNetwork && !_hideWebSocket)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Use WebSocket')),
|
||||
initialValue: _allowWebSocket,
|
||||
onToggle: isOptionFixed(kOptionAllowWebSocket)
|
||||
? null
|
||||
: (v) async {
|
||||
await mainSetBoolOption(kOptionAllowWebSocket, v);
|
||||
final newValue =
|
||||
await mainGetBoolOption(kOptionAllowWebSocket);
|
||||
setState(() {
|
||||
_allowWebSocket = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!incomingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Enable UDP hole punching')),
|
||||
initialValue: _enableUdpPunch,
|
||||
onToggle: (v) async {
|
||||
await mainSetLocalBoolOption(kOptionEnableUdpPunch, v);
|
||||
final newValue =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
||||
setState(() {
|
||||
_enableUdpPunch = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!incomingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Enable IPv6 P2P connection')),
|
||||
initialValue: _enableIpv6Punch,
|
||||
onToggle: (v) async {
|
||||
await mainSetLocalBoolOption(kOptionEnableIpv6Punch, v);
|
||||
final newValue =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
||||
setState(() {
|
||||
_enableIpv6Punch = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text(translate('Language')),
|
||||
leading: Icon(Icons.translate),
|
||||
@@ -728,7 +778,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!incommingOnly)
|
||||
if (!incomingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title:
|
||||
Text(translate('Automatically record outgoing sessions')),
|
||||
@@ -765,7 +815,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
!outgoingOnly &&
|
||||
!hideSecuritySettings)
|
||||
SettingsSection(
|
||||
title: Text(translate("Share Screen")),
|
||||
title: Text(translate("Share screen")),
|
||||
tiles: shareScreenTiles,
|
||||
),
|
||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||
|
||||
125
flutter/lib/mobile/pages/terminal_page.dart
Normal file
125
flutter/lib/mobile/pages/terminal_page.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
import '../../desktop/pages/terminal_connection_manager.dart';
|
||||
|
||||
class TerminalPage extends StatefulWidget {
|
||||
const TerminalPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.isSharedPassword,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final terminalId = 0;
|
||||
|
||||
@override
|
||||
State<TerminalPage> createState() => _TerminalPageState();
|
||||
}
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
|
||||
// For web only.
|
||||
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
||||
final String _robotoMonoFontFamily = isWeb
|
||||
? (GoogleFonts.robotoMono().fontFamily ?? 'monospace')
|
||||
: 'monospace';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
debugPrint(
|
||||
'[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}');
|
||||
|
||||
// Use shared FFI instance from connection manager
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
peerId: widget.id,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
);
|
||||
|
||||
// Create terminal model with specific terminal ID
|
||||
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
||||
debugPrint(
|
||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Unregister terminal model from FFI
|
||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||
_terminalModel.dispose();
|
||||
super.dispose();
|
||||
TerminalConnectionManager.releaseConnection(widget.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
textStyle: _getTerminalStyle(),
|
||||
backgroundOpacity: 0.7,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/TerminalStudio/xterm.dart/issues/42#issuecomment-877495472
|
||||
// https://github.com/TerminalStudio/xterm.dart/issues/198#issuecomment-2526548458
|
||||
TerminalStyle _getTerminalStyle() {
|
||||
return isWeb
|
||||
? TerminalStyle(
|
||||
fontFamily: _robotoMonoFontFamily,
|
||||
fontSize: 14,
|
||||
)
|
||||
: const TerminalStyle();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -39,12 +39,13 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
|
||||
|
||||
class ViewCameraPage extends StatefulWidget {
|
||||
ViewCameraPage(
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
|
||||
: super(key: key);
|
||||
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
|
||||
@override
|
||||
State<ViewCameraPage> createState() => _ViewCameraPageState(id);
|
||||
@@ -88,6 +89,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
isViewCamera: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
@@ -478,9 +480,9 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
);
|
||||
if (index != null) {
|
||||
if (index < mobileActionMenus.length) {
|
||||
mobileActionMenus[index].onPressed.call();
|
||||
mobileActionMenus[index].onPressed?.call();
|
||||
} else if (index < mobileActionMenus.length + more.length) {
|
||||
menus[index - mobileActionMenus.length].onPressed.call();
|
||||
menus[index - mobileActionMenus.length].onPressed?.call();
|
||||
}
|
||||
}
|
||||
}();
|
||||
@@ -553,7 +555,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
elevation: 8,
|
||||
);
|
||||
if (index != null && index < menus.length) {
|
||||
menus[index].onPressed.call();
|
||||
menus[index].onPressed?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -347,6 +347,9 @@ class AbModel {
|
||||
if (ab == null) {
|
||||
return 'no such addressbook: $name';
|
||||
}
|
||||
for (var p in ps) {
|
||||
ab.removeNonExistentTags(p);
|
||||
}
|
||||
String? errMsg = await ab.addPeers(ps);
|
||||
await pullNonLegacyAfterChange(name: name);
|
||||
if (name == _currentName.value) {
|
||||
@@ -775,7 +778,10 @@ abstract class BaseAb {
|
||||
|
||||
final pullError = "".obs;
|
||||
final pushError = "".obs;
|
||||
final abLoading = false.obs;
|
||||
final abLoading = false
|
||||
.obs; // Indicates whether the UI should show a loading state for the address book.
|
||||
var abPulling =
|
||||
false; // Tracks whether a pull operation is currently in progress to prevent concurrent pulls. Unlike abLoading, this is not tied to UI updates.
|
||||
bool initialized = false;
|
||||
|
||||
String name();
|
||||
@@ -790,17 +796,22 @@ abstract class BaseAb {
|
||||
}
|
||||
|
||||
Future<void> pullAb({quiet = false}) async {
|
||||
debugPrint("pull ab \"${name()}\"");
|
||||
if (abLoading.value) return;
|
||||
if (abPulling) return;
|
||||
abPulling = true;
|
||||
if (!quiet) {
|
||||
abLoading.value = true;
|
||||
pullError.value = "";
|
||||
}
|
||||
initialized = false;
|
||||
debugPrint("pull ab \"${name()}\"");
|
||||
try {
|
||||
initialized = await pullAbImpl(quiet: quiet);
|
||||
} catch (_) {}
|
||||
abLoading.value = false;
|
||||
} catch (e) {
|
||||
debugPrint("Error occurred while pulling address book: $e");
|
||||
} finally {
|
||||
abLoading.value = false;
|
||||
abPulling = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> pullAbImpl({quiet = false});
|
||||
@@ -814,6 +825,18 @@ abstract class BaseAb {
|
||||
p.remove('password');
|
||||
}
|
||||
|
||||
removeNonExistentTags(Map<String, dynamic> p) {
|
||||
try {
|
||||
final oldTags = p.remove('tags');
|
||||
if (oldTags is List) {
|
||||
final newTags = oldTags.where((e) => tagContainBy(e)).toList();
|
||||
p['tags'] = newTags;
|
||||
}
|
||||
} catch (e) {
|
||||
print("removeNonExistentTags: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> changeTagForPeers(List<String> ids, List<dynamic> tags);
|
||||
|
||||
Future<bool> changeAlias({required String id, required String alias});
|
||||
|
||||
@@ -47,7 +47,10 @@ class GroupModel {
|
||||
}
|
||||
try {
|
||||
await _pull();
|
||||
} catch (_) {}
|
||||
_tryHandlePullError();
|
||||
} catch (e) {
|
||||
print("pull accessibles error: $e");
|
||||
}
|
||||
groupLoading.value = false;
|
||||
initialized = true;
|
||||
platformFFI.tryHandle({'name': LoadEvent.group});
|
||||
@@ -361,4 +364,14 @@ class GroupModel {
|
||||
void removePeerUpdateListener(String key) {
|
||||
_peerIdUpdateListeners.remove(key);
|
||||
}
|
||||
|
||||
void _tryHandlePullError() {
|
||||
String errorMessage = groupLoadError.value;
|
||||
// The error message is "Retrieving accessible devices is disabled."
|
||||
if (errorMessage.toLowerCase().contains('disabled')) {
|
||||
users.clear();
|
||||
peers.clear();
|
||||
deviceGroups.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,8 +345,11 @@ class InputModel {
|
||||
var _fling = false;
|
||||
Timer? _flingTimer;
|
||||
final _flingBaseDelay = 30;
|
||||
// trackpad, peer linux
|
||||
final _trackpadSpeed = 0.06;
|
||||
final _trackpadAdjustPeerLinux = 0.06;
|
||||
// This is an experience value.
|
||||
final _trackpadAdjustMacToWin = 2.50;
|
||||
int _trackpadSpeed = kDefaultTrackpadSpeed;
|
||||
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
|
||||
var _trackpadScrollUnsent = Offset.zero;
|
||||
|
||||
var _lastScale = 1.0;
|
||||
@@ -370,6 +373,7 @@ class InputModel {
|
||||
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
||||
int get trackpadSpeed => _trackpadSpeed;
|
||||
|
||||
InputModel(this.parent) {
|
||||
sessionId = parent.target!.sessionId;
|
||||
@@ -385,6 +389,28 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the trackpad speed based on the session value.
|
||||
///
|
||||
/// The expected format of the retrieved value is a string that can be parsed into a double.
|
||||
/// If parsing fails or the value is out of bounds (less than `kMinTrackpadSpeed` or greater
|
||||
/// than `kMaxTrackpadSpeed`), the trackpad speed is reset to the default
|
||||
/// value (`kDefaultTrackpadSpeed`).
|
||||
///
|
||||
/// Bounds:
|
||||
/// - Minimum: `kMinTrackpadSpeed`
|
||||
/// - Maximum: `kMaxTrackpadSpeed`
|
||||
/// - Default: `kDefaultTrackpadSpeed`
|
||||
Future<void> updateTrackpadSpeed() async {
|
||||
_trackpadSpeed =
|
||||
(await bind.sessionGetTrackpadSpeed(sessionId: sessionId) ??
|
||||
kDefaultTrackpadSpeed);
|
||||
if (_trackpadSpeed < kMinTrackpadSpeed ||
|
||||
_trackpadSpeed > kMaxTrackpadSpeed) {
|
||||
_trackpadSpeed = kDefaultTrackpadSpeed;
|
||||
}
|
||||
_trackpadSpeedInner = _trackpadSpeed / 100.0;
|
||||
}
|
||||
|
||||
void handleKeyDownEventModifiers(KeyEvent e) {
|
||||
KeyUpEvent upEvent(e) => KeyUpEvent(
|
||||
physicalKey: e.physicalKey,
|
||||
@@ -888,13 +914,16 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
final delta = e.panDelta;
|
||||
var delta = e.panDelta * _trackpadSpeedInner;
|
||||
if (isMacOS && peerPlatform == kPeerPlatformWindows) {
|
||||
delta *= _trackpadAdjustMacToWin;
|
||||
}
|
||||
_trackpadLastDelta = delta;
|
||||
|
||||
var x = delta.dx.toInt();
|
||||
var y = delta.dy.toInt();
|
||||
if (peerPlatform == kPeerPlatformLinux) {
|
||||
_trackpadScrollUnsent += (delta * _trackpadSpeed);
|
||||
_trackpadScrollUnsent += (delta * _trackpadAdjustPeerLinux);
|
||||
x = _trackpadScrollUnsent.dx.truncate();
|
||||
y = _trackpadScrollUnsent.dy.truncate();
|
||||
_trackpadScrollUnsent -= Offset(x.toDouble(), y.toDouble());
|
||||
@@ -942,8 +971,8 @@ class InputModel {
|
||||
var dx = x.toInt();
|
||||
var dy = y.toInt();
|
||||
if (parent.target?.ffiModel.pi.platform == kPeerPlatformLinux) {
|
||||
dx = (x * _trackpadSpeed).toInt();
|
||||
dy = (y * _trackpadSpeed).toInt();
|
||||
dx = (x * _trackpadAdjustPeerLinux).toInt();
|
||||
dy = (y * _trackpadAdjustPeerLinux).toInt();
|
||||
}
|
||||
|
||||
var delay = _flingBaseDelay;
|
||||
@@ -989,7 +1018,10 @@ class InputModel {
|
||||
_stopFling = false;
|
||||
|
||||
// 2.0 is an experience value
|
||||
double minFlingValue = 2.0;
|
||||
double minFlingValue = 2.0 * _trackpadSpeedInner;
|
||||
if (isMacOS && peerPlatform == kPeerPlatformWindows) {
|
||||
minFlingValue *= _trackpadAdjustMacToWin;
|
||||
}
|
||||
if (_trackpadLastDelta.dx.abs() > minFlingValue ||
|
||||
_trackpadLastDelta.dy.abs() > minFlingValue) {
|
||||
_fling = true;
|
||||
|
||||
@@ -23,6 +23,7 @@ import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/user_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:flutter_hbb/plugin/event.dart';
|
||||
import 'package:flutter_hbb/plugin/manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||
@@ -34,6 +35,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
import '../common.dart';
|
||||
import '../utils/image.dart' as img;
|
||||
@@ -119,6 +121,8 @@ class FfiModel with ChangeNotifier {
|
||||
RxBool waitForFirstImage = true.obs;
|
||||
bool isRefreshing = false;
|
||||
|
||||
Timer? timerScreenshot;
|
||||
|
||||
Rect? get rect => _rect;
|
||||
bool get isOriginalResolutionSet =>
|
||||
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolutionSet ?? false;
|
||||
@@ -216,6 +220,7 @@ class FfiModel with ChangeNotifier {
|
||||
_timer = null;
|
||||
clearPermissions();
|
||||
waitForImageTimer?.cancel();
|
||||
timerScreenshot?.cancel();
|
||||
}
|
||||
|
||||
setConnectionType(String peerId, bool secure, bool direct) {
|
||||
@@ -307,6 +312,8 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'chat_server_mode') {
|
||||
parent.target?.chatModel
|
||||
.receive(int.parse(evt['id'] as String), evt['text'] ?? '');
|
||||
} else if (name == 'terminal_response') {
|
||||
parent.target?.routeTerminalResponse(evt);
|
||||
} else if (name == 'file_dir') {
|
||||
parent.target?.fileModel.receiveFileDir(evt);
|
||||
} else if (name == 'empty_dirs') {
|
||||
@@ -414,12 +421,82 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
} else if (name == "printer_request") {
|
||||
_handlePrinterRequest(evt, sessionId, peerId);
|
||||
} else if (name == 'screenshot') {
|
||||
_handleScreenshot(evt, sessionId, peerId);
|
||||
} else {
|
||||
debugPrint('Event is not handled in the fixed branch: $name');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_handleScreenshot(
|
||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
timerScreenshot?.cancel();
|
||||
timerScreenshot = null;
|
||||
final msg = evt['msg'] ?? '';
|
||||
final msgBoxType = 'custom-nook-nocancel-hasclose';
|
||||
final msgBoxTitle = 'Take screenshot';
|
||||
final dialogManager = parent.target!.dialogManager;
|
||||
if (msg.isNotEmpty) {
|
||||
msgBox(sessionId, msgBoxType, msgBoxTitle, msg, '', dialogManager);
|
||||
} else {
|
||||
final msgBoxText = 'screenshot-action-tip';
|
||||
|
||||
close() {
|
||||
dialogManager.dismissAll();
|
||||
}
|
||||
|
||||
saveAs() {
|
||||
close();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
final ts = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
String? outputFile = await FilePicker.platform.saveFile(
|
||||
dialogTitle: '${translate('Save as')}...',
|
||||
fileName: 'screenshot_$ts.png',
|
||||
allowedExtensions: ['png'],
|
||||
type: FileType.custom,
|
||||
);
|
||||
if (outputFile == null) {
|
||||
bind.sessionHandleScreenshot(sessionId: sessionId, action: '2');
|
||||
} else {
|
||||
final res = await bind.sessionHandleScreenshot(
|
||||
sessionId: sessionId, action: '0:$outputFile');
|
||||
if (res.isNotEmpty) {
|
||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-error',
|
||||
'Take screenshot', res, '', dialogManager);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
copyToClipboard() {
|
||||
bind.sessionHandleScreenshot(sessionId: sessionId, action: '1');
|
||||
close();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
bind.sessionHandleScreenshot(sessionId: sessionId, action: '2');
|
||||
close();
|
||||
}
|
||||
|
||||
final List<Widget> buttons = [
|
||||
dialogButton('${translate('Save as')}...', onPressed: saveAs),
|
||||
dialogButton('Copy to clipboard', onPressed: copyToClipboard),
|
||||
dialogButton('Cancel', onPressed: cancel),
|
||||
];
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show(
|
||||
(setState, close, context) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: SelectionArea(
|
||||
child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)),
|
||||
actions: buttons,
|
||||
),
|
||||
tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_handlePrinterRequest(
|
||||
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
final id = evt['id'];
|
||||
@@ -451,7 +528,7 @@ class FfiModel with ChangeNotifier {
|
||||
if (saveSettings.value || dontShowAgain.value) {
|
||||
bind.mainSetLocalOption(key: kKeyPrinterSelected, value: printerName);
|
||||
bind.mainSetLocalOption(
|
||||
key: kKeyPrinterIncommingJobAction,
|
||||
key: kKeyPrinterIncomingJobAction,
|
||||
value: defaultOrSelectedGroupValue.value);
|
||||
}
|
||||
if (dontShowAgain.value) {
|
||||
@@ -463,7 +540,7 @@ class FfiModel with ChangeNotifier {
|
||||
onCancel() {
|
||||
if (dontShowAgain.value) {
|
||||
bind.mainSetLocalOption(
|
||||
key: kKeyPrinterIncommingJobAction,
|
||||
key: kKeyPrinterIncomingJobAction,
|
||||
value: kValuePrinterIncomingJobDismiss);
|
||||
}
|
||||
close();
|
||||
@@ -759,10 +836,16 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (type == 'input-password') {
|
||||
enterPasswordDialog(sessionId, dialogManager);
|
||||
} else if (type == 'session-login' || type == 'session-re-login') {
|
||||
enterUserLoginDialog(sessionId, dialogManager);
|
||||
} else if (type == 'session-login-password' ||
|
||||
type == 'session-login-password') {
|
||||
enterUserLoginAndPasswordDialog(sessionId, dialogManager);
|
||||
enterUserLoginDialog(sessionId, dialogManager, 'login_linux_tip', true);
|
||||
} else if (type == 'session-login-password') {
|
||||
enterUserLoginAndPasswordDialog(
|
||||
sessionId, dialogManager, 'login_linux_tip', true);
|
||||
} else if (type == 'terminal-admin-login') {
|
||||
enterUserLoginDialog(
|
||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||
} else if (type == 'terminal-admin-login-password') {
|
||||
enterUserLoginAndPasswordDialog(
|
||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||
} else if (type == 'restarting') {
|
||||
showMsgBox(sessionId, type, title, text, link, false, dialogManager,
|
||||
hasCancel: false);
|
||||
@@ -917,17 +1000,12 @@ class FfiModel with ChangeNotifier {
|
||||
String link,
|
||||
bool hasRetry,
|
||||
OverlayDialogManager dialogManager) {
|
||||
if (text == 'no_need_privacy_mode_no_physical_displays_tip' ||
|
||||
text == 'Enter privacy mode') {
|
||||
// There are display changes on the remote side,
|
||||
// which will cause some messages to refresh the canvas and dismiss dialogs.
|
||||
// So we add a delay here to ensure the dialog is displayed.
|
||||
Future.delayed(Duration(milliseconds: 3000), () {
|
||||
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||
});
|
||||
} else {
|
||||
// There are display changes on the remote side,
|
||||
// which will cause some messages to refresh the canvas and dismiss dialogs.
|
||||
// So we add a delay here to ensure the dialog is displayed.
|
||||
Future.delayed(Duration(milliseconds: 3000), () {
|
||||
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateSessionWidthHeight(SessionID sessionId) {
|
||||
@@ -1007,9 +1085,14 @@ class FfiModel with ChangeNotifier {
|
||||
sessionId: sessionId, arg: kOptionTouchMode) !=
|
||||
'';
|
||||
}
|
||||
// FIXME: handle ViewCamera ConnType independently.
|
||||
if (connType == ConnType.fileTransfer) {
|
||||
parent.target?.fileModel.onReady();
|
||||
} else if (connType == ConnType.terminal) {
|
||||
// Call onReady on all registered terminal models
|
||||
final models = parent.target?._terminalModels.values ?? [];
|
||||
for (final model in models) {
|
||||
model.onReady();
|
||||
}
|
||||
} else if (connType == ConnType.defaultConn ||
|
||||
connType == ConnType.viewCamera) {
|
||||
List<Display> newDisplays = [];
|
||||
@@ -2759,7 +2842,14 @@ class ElevationModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
// The index values of `ConnType` are same as rust protobuf.
|
||||
enum ConnType { defaultConn, fileTransfer, portForward, rdp, viewCamera }
|
||||
enum ConnType {
|
||||
defaultConn,
|
||||
fileTransfer,
|
||||
portForward,
|
||||
rdp,
|
||||
viewCamera,
|
||||
terminal
|
||||
}
|
||||
|
||||
/// Flutter state manager and data communication with the Rust core.
|
||||
class FFI {
|
||||
@@ -2794,6 +2884,12 @@ class FFI {
|
||||
late final Peers favoritePeersModel; // global
|
||||
late final Peers lanPeersModel; // global
|
||||
|
||||
// Terminal model registry for multiple terminals
|
||||
final Map<int, TerminalModel> _terminalModels = {};
|
||||
|
||||
// Getter for terminal models
|
||||
Map<int, TerminalModel> get terminalModels => _terminalModels;
|
||||
|
||||
FFI(SessionID? sId) {
|
||||
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
|
||||
imageModel = ImageModel(WeakReference(this));
|
||||
@@ -2841,6 +2937,7 @@ class FFI {
|
||||
bool isViewCamera = false,
|
||||
bool isPortForward = false,
|
||||
bool isRdp = false,
|
||||
bool isTerminal = false,
|
||||
String? switchUuid,
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
@@ -2856,7 +2953,10 @@ class FFI {
|
||||
assert(
|
||||
(!(isPortForward && isViewCamera)) &&
|
||||
(!(isViewCamera && isPortForward)) &&
|
||||
(!(isPortForward && isFileTransfer)),
|
||||
(!(isPortForward && isFileTransfer)) &&
|
||||
(!(isTerminal && isFileTransfer)) &&
|
||||
(!(isTerminal && isViewCamera)) &&
|
||||
(!(isTerminal && isPortForward)),
|
||||
'more than one connect type');
|
||||
if (isFileTransfer) {
|
||||
connType = ConnType.fileTransfer;
|
||||
@@ -2864,6 +2964,8 @@ class FFI {
|
||||
connType = ConnType.viewCamera;
|
||||
} else if (isPortForward) {
|
||||
connType = ConnType.portForward;
|
||||
} else if (isTerminal) {
|
||||
connType = ConnType.terminal;
|
||||
} else {
|
||||
chatModel.resetClientMode();
|
||||
connType = ConnType.defaultConn;
|
||||
@@ -2884,6 +2986,7 @@ class FFI {
|
||||
isViewCamera: isViewCamera,
|
||||
isPortForward: isPortForward,
|
||||
isRdp: isRdp,
|
||||
isTerminal: isTerminal,
|
||||
switchUuid: switchUuid ?? '',
|
||||
forceRelay: forceRelay ?? false,
|
||||
password: password ?? '',
|
||||
@@ -2917,6 +3020,10 @@ class FFI {
|
||||
textureModel.updateCurrentDisplay(display ?? 0);
|
||||
}
|
||||
|
||||
if (isDesktop) {
|
||||
inputModel.updateTrackpadSpeed();
|
||||
}
|
||||
|
||||
// CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions.
|
||||
// Though the stream is returned immediately, the stream may not be ready.
|
||||
// Any operations that depend on the stream should be carefully handled.
|
||||
@@ -3059,6 +3166,11 @@ class FFI {
|
||||
Future<void> close({bool closeSession = true}) async {
|
||||
closed = true;
|
||||
chatModel.close();
|
||||
// Close all terminal models
|
||||
for (final model in _terminalModels.values) {
|
||||
model.dispose();
|
||||
}
|
||||
_terminalModels.clear();
|
||||
if (imageModel.image != null && !isWebDesktop) {
|
||||
await setCanvasConfig(
|
||||
sessionId,
|
||||
@@ -3089,6 +3201,27 @@ class FFI {
|
||||
Future<bool> invokeMethod(String method, [dynamic arguments]) async {
|
||||
return await platformFFI.invokeMethod(method, arguments);
|
||||
}
|
||||
|
||||
// Terminal model management
|
||||
void registerTerminalModel(int terminalId, TerminalModel model) {
|
||||
debugPrint('[FFI] Registering terminal model for terminal $terminalId');
|
||||
_terminalModels[terminalId] = model;
|
||||
}
|
||||
|
||||
void unregisterTerminalModel(int terminalId) {
|
||||
debugPrint('[FFI] Unregistering terminal model for terminal $terminalId');
|
||||
_terminalModels.remove(terminalId);
|
||||
}
|
||||
|
||||
void routeTerminalResponse(Map<String, dynamic> evt) {
|
||||
final int terminalId = TerminalModel.getTerminalIdFromEvt(evt);
|
||||
|
||||
// Route to specific terminal model if it exists
|
||||
final model = _terminalModels[terminalId];
|
||||
if (model != null) {
|
||||
model.handleTerminalResponse(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kInvalidResolutionValue = -1;
|
||||
@@ -3134,7 +3267,8 @@ class Display {
|
||||
originalWidth == kVirtualDisplayResolutionValue &&
|
||||
originalHeight == kVirtualDisplayResolutionValue;
|
||||
bool get isOriginalResolution =>
|
||||
width == originalWidth && height == originalHeight;
|
||||
width == (originalWidth * scale).round() &&
|
||||
height == (originalHeight * scale).round();
|
||||
}
|
||||
|
||||
class Resolution {
|
||||
@@ -3192,9 +3326,6 @@ class PeerInfo with ChangeNotifier {
|
||||
bool get isAmyuniIdd =>
|
||||
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
||||
|
||||
bool get isSupportViewCamera =>
|
||||
platformAdditions[kPlatformAdditionsSupportViewCamera] == true;
|
||||
|
||||
Display? tryGetDisplay({int? display}) {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
|
||||
@@ -13,7 +13,7 @@ class PrinterOptions {
|
||||
required this.printerName});
|
||||
|
||||
static PrinterOptions load() {
|
||||
var action = bind.mainGetLocalOption(key: kKeyPrinterIncommingJobAction);
|
||||
var action = bind.mainGetLocalOption(key: kKeyPrinterIncomingJobAction);
|
||||
if (![
|
||||
kValuePrinterIncomingJobDismiss,
|
||||
kValuePrinterIncomingJobDefault,
|
||||
@@ -28,7 +28,7 @@ class PrinterOptions {
|
||||
if (action == kValuePrinterIncomingJobSelected) {
|
||||
action = kValuePrinterIncomingJobDefault;
|
||||
bind.mainSetLocalOption(
|
||||
key: kKeyPrinterIncommingJobAction,
|
||||
key: kKeyPrinterIncomingJobAction,
|
||||
value: kValuePrinterIncomingJobDefault);
|
||||
if (printerNames.isEmpty) {
|
||||
selectedPrinterName = '';
|
||||
|
||||
@@ -36,6 +36,7 @@ class ServerModel with ChangeNotifier {
|
||||
int _connectStatus = 0; // Rendezvous Server status
|
||||
String _verificationMethod = "";
|
||||
String _temporaryPasswordLength = "";
|
||||
bool _allowNumericOneTimePassword = false;
|
||||
String _approveMode = "";
|
||||
int _zeroClientLengthCounter = 0;
|
||||
|
||||
@@ -112,6 +113,12 @@ class ServerModel with ChangeNotifier {
|
||||
*/
|
||||
}
|
||||
|
||||
bool get allowNumericOneTimePassword => _allowNumericOneTimePassword;
|
||||
switchAllowNumericOneTimePassword() async {
|
||||
await mainSetBoolOption(
|
||||
kOptionAllowNumericOneTimePassword, !_allowNumericOneTimePassword);
|
||||
}
|
||||
|
||||
TextEditingController get serverId => _serverId;
|
||||
|
||||
TextEditingController get serverPasswd => _serverPasswd;
|
||||
@@ -227,6 +234,8 @@ class ServerModel with ChangeNotifier {
|
||||
final temporaryPasswordLength =
|
||||
await bind.mainGetOption(key: "temporary-password-length");
|
||||
final approveMode = await bind.mainGetOption(key: kOptionApproveMode);
|
||||
final numericOneTimePassword =
|
||||
await mainGetBoolOption(kOptionAllowNumericOneTimePassword);
|
||||
/*
|
||||
var hideCm = option2bool(
|
||||
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
|
||||
@@ -265,6 +274,10 @@ class ServerModel with ChangeNotifier {
|
||||
_temporaryPasswordLength = temporaryPasswordLength;
|
||||
update = true;
|
||||
}
|
||||
if (_allowNumericOneTimePassword != numericOneTimePassword) {
|
||||
_allowNumericOneTimePassword = numericOneTimePassword;
|
||||
update = true;
|
||||
}
|
||||
/*
|
||||
if (_hideCm != hideCm) {
|
||||
_hideCm = hideCm;
|
||||
@@ -600,7 +613,13 @@ class ServerModel with ChangeNotifier {
|
||||
void showLoginDialog(Client client) {
|
||||
showClientDialog(
|
||||
client,
|
||||
client.isFileTransfer ? "File Connection" : "Screen Connection",
|
||||
client.isFileTransfer
|
||||
? "Transfer file"
|
||||
: client.isViewCamera
|
||||
? "View camera"
|
||||
: client.isTerminal
|
||||
? "Terminal"
|
||||
: "Share screen",
|
||||
'Do you accept?',
|
||||
'android_new_connection_tip',
|
||||
() => sendLoginResponse(client, false),
|
||||
@@ -679,7 +698,7 @@ class ServerModel with ChangeNotifier {
|
||||
void sendLoginResponse(Client client, bool res) async {
|
||||
if (res) {
|
||||
bind.cmLoginRes(connId: client.id, res: res);
|
||||
if (!client.isFileTransfer) {
|
||||
if (!client.isFileTransfer && !client.isTerminal) {
|
||||
parent.target?.invokeMethod("start_capture");
|
||||
}
|
||||
parent.target?.invokeMethod("cancel_notification", client.id);
|
||||
@@ -793,6 +812,7 @@ enum ClientType {
|
||||
file,
|
||||
camera,
|
||||
portForward,
|
||||
terminal,
|
||||
}
|
||||
|
||||
class Client {
|
||||
@@ -800,6 +820,7 @@ class Client {
|
||||
bool authorized = false;
|
||||
bool isFileTransfer = false;
|
||||
bool isViewCamera = false;
|
||||
bool isTerminal = false;
|
||||
String portForward = "";
|
||||
String name = "";
|
||||
String peerId = ""; // peer user's id,show at app
|
||||
@@ -817,8 +838,8 @@ class Client {
|
||||
|
||||
RxInt unreadChatMessageCount = 0.obs;
|
||||
|
||||
Client(this.id, this.authorized, this.isFileTransfer, this.isViewCamera, this.name, this.peerId,
|
||||
this.keyboard, this.clipboard, this.audio);
|
||||
Client(this.id, this.authorized, this.isFileTransfer, this.isViewCamera,
|
||||
this.name, this.peerId, this.keyboard, this.clipboard, this.audio);
|
||||
|
||||
Client.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
@@ -826,6 +847,7 @@ class Client {
|
||||
isFileTransfer = json['is_file_transfer'];
|
||||
// TODO: no entry then default.
|
||||
isViewCamera = json['is_view_camera'];
|
||||
isTerminal = json['is_terminal'] ?? false;
|
||||
portForward = json['port_forward'];
|
||||
name = json['name'];
|
||||
peerId = json['peer_id'];
|
||||
@@ -848,6 +870,7 @@ class Client {
|
||||
data['authorized'] = authorized;
|
||||
data['is_file_transfer'] = isFileTransfer;
|
||||
data['is_view_camera'] = isViewCamera;
|
||||
data['is_terminal'] = isTerminal;
|
||||
data['port_forward'] = portForward;
|
||||
data['name'] = name;
|
||||
data['peer_id'] = peerId;
|
||||
@@ -870,6 +893,8 @@ class Client {
|
||||
return ClientType.file;
|
||||
} else if (isViewCamera) {
|
||||
return ClientType.camera;
|
||||
} else if (isTerminal) {
|
||||
return ClientType.terminal;
|
||||
} else if (portForward.isNotEmpty) {
|
||||
return ClientType.portForward;
|
||||
} else {
|
||||
|
||||
346
flutter/lib/models/terminal_model.dart
Normal file
346
flutter/lib/models/terminal_model.dart
Normal file
@@ -0,0 +1,346 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
import 'model.dart';
|
||||
import 'platform_model.dart';
|
||||
|
||||
class TerminalModel with ChangeNotifier {
|
||||
final String id; // peer id
|
||||
final FFI parent;
|
||||
final int terminalId;
|
||||
late final Terminal terminal;
|
||||
late final TerminalController terminalController;
|
||||
|
||||
bool _terminalOpened = false;
|
||||
bool get terminalOpened => _terminalOpened;
|
||||
|
||||
bool _disposed = false;
|
||||
|
||||
final _inputBuffer = <String>[];
|
||||
|
||||
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
|
||||
|
||||
Future<void> _handleInput(String data) async {
|
||||
// If we press the `Enter` button on Android,
|
||||
// `data` can be '\r' or '\n' when using different keyboards.
|
||||
// Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline.
|
||||
// Android -> Linux. Both '\r' and '\n' work as expected (execute a command).
|
||||
// So when we receive '\n', we may need to convert it to '\r' to ensure compatibility.
|
||||
// Desktop -> Desktop works fine.
|
||||
// Check if we are on mobile or web(mobile), and convert '\n' to '\r'.
|
||||
final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop));
|
||||
if (isMobileOrWebMobile && isPeerWindows && data == '\n') {
|
||||
data = '\r';
|
||||
}
|
||||
if (_terminalOpened) {
|
||||
// Send user input to remote terminal
|
||||
try {
|
||||
await bind.sessionSendTerminalInput(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
data: data,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error sending terminal input: $e');
|
||||
}
|
||||
} else {
|
||||
debugPrint('[TerminalModel] Terminal not opened yet, buffering input');
|
||||
_inputBuffer.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
TerminalModel(this.parent, [this.terminalId = 0]) : id = parent.id {
|
||||
terminal = Terminal(maxLines: 10000);
|
||||
terminalController = TerminalController();
|
||||
|
||||
// Setup terminal callbacks
|
||||
terminal.onOutput = _handleInput;
|
||||
|
||||
terminal.onResize = (w, h, pw, ph) async {
|
||||
// Validate all dimensions before using them
|
||||
if (w > 0 && h > 0 && pw > 0 && ph > 0) {
|
||||
debugPrint(
|
||||
'[TerminalModel] Terminal resized to ${w}x$h (pixel: ${pw}x$ph)');
|
||||
if (_terminalOpened) {
|
||||
// Notify remote terminal of resize
|
||||
try {
|
||||
await bind.sessionResizeTerminal(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
rows: h,
|
||||
cols: w,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error resizing terminal: $e');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[TerminalModel] Invalid terminal dimensions: ${w}x$h (pixel: ${pw}x$ph)');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void onReady() {
|
||||
parent.dialogManager.dismissAll();
|
||||
|
||||
// Fire and forget - don't block onReady
|
||||
openTerminal().catchError((e) {
|
||||
debugPrint('[TerminalModel] Error opening terminal: $e');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> openTerminal() async {
|
||||
if (_terminalOpened) return;
|
||||
// Request the remote side to open a terminal with default shell
|
||||
// The remote side will decide which shell to use based on its OS
|
||||
|
||||
// Get terminal dimensions, ensuring they are valid
|
||||
int rows = 24;
|
||||
int cols = 80;
|
||||
|
||||
if (terminal.viewHeight > 0) {
|
||||
rows = terminal.viewHeight;
|
||||
}
|
||||
if (terminal.viewWidth > 0) {
|
||||
cols = terminal.viewWidth;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[TerminalModel] Opening terminal $terminalId, sessionId: ${parent.sessionId}, size: ${cols}x$rows');
|
||||
try {
|
||||
await bind
|
||||
.sessionOpenTerminal(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
rows: rows,
|
||||
cols: cols,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
throw TimeoutException(
|
||||
'sessionOpenTerminal timed out after 5 seconds');
|
||||
},
|
||||
);
|
||||
debugPrint('[TerminalModel] sessionOpenTerminal called successfully');
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e');
|
||||
// Optionally show error to user
|
||||
if (e is TimeoutException) {
|
||||
terminal.write('Failed to open terminal: Connection timeout\r\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeTerminal() async {
|
||||
if (_terminalOpened) {
|
||||
try {
|
||||
await bind
|
||||
.sessionCloseTerminal(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () {
|
||||
throw TimeoutException(
|
||||
'sessionCloseTerminal timed out after 3 seconds');
|
||||
},
|
||||
);
|
||||
debugPrint('[TerminalModel] sessionCloseTerminal called successfully');
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error calling sessionCloseTerminal: $e');
|
||||
// Continue with cleanup even if close fails
|
||||
}
|
||||
_terminalOpened = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
static int getTerminalIdFromEvt(Map<String, dynamic> evt) {
|
||||
if (evt.containsKey('terminal_id')) {
|
||||
final v = evt['terminal_id'];
|
||||
if (v is int) {
|
||||
// Desktop and mobile send terminal_id as an int
|
||||
return v;
|
||||
} else if (v is String) {
|
||||
// Web sends terminal_id as a string
|
||||
final parsed = int.tryParse(v);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
} else {
|
||||
debugPrint(
|
||||
'[TerminalModel] Failed to parse terminal_id as integer: $v. Expected a numeric string.');
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
// Unexpected type, log and handle gracefully
|
||||
debugPrint(
|
||||
'[TerminalModel] Unexpected terminal_id type: ${v.runtimeType}, value: $v. Expected int or String.');
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
debugPrint('[TerminalModel] Event does not contain terminal_id');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool getSuccessFromEvt(Map<String, dynamic> evt) {
|
||||
if (evt.containsKey('success')) {
|
||||
final v = evt['success'];
|
||||
if (v is bool) {
|
||||
// Desktop and mobile
|
||||
return v;
|
||||
} else if (v is String) {
|
||||
// Web
|
||||
return v.toLowerCase() == 'true';
|
||||
} else {
|
||||
// Unexpected type, log and handle gracefully
|
||||
debugPrint(
|
||||
'[TerminalModel] Unexpected success type: ${v.runtimeType}, value: $v. Expected bool or String.');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
debugPrint('[TerminalModel] Event does not contain success');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void handleTerminalResponse(Map<String, dynamic> evt) {
|
||||
final String? type = evt['type'];
|
||||
final int evtTerminalId = getTerminalIdFromEvt(evt);
|
||||
|
||||
// Only handle events for this terminal
|
||||
if (evtTerminalId != terminalId) {
|
||||
debugPrint(
|
||||
'[TerminalModel] Ignoring event for terminal $evtTerminalId (not mine)');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'opened':
|
||||
_handleTerminalOpened(evt);
|
||||
break;
|
||||
case 'data':
|
||||
_handleTerminalData(evt);
|
||||
break;
|
||||
case 'closed':
|
||||
_handleTerminalClosed(evt);
|
||||
break;
|
||||
case 'error':
|
||||
_handleTerminalError(evt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTerminalOpened(Map<String, dynamic> evt) {
|
||||
final bool success = getSuccessFromEvt(evt);
|
||||
final String message = evt['message'] ?? '';
|
||||
final String? serviceId = evt['service_id'];
|
||||
|
||||
debugPrint(
|
||||
'[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId');
|
||||
|
||||
if (success) {
|
||||
_terminalOpened = true;
|
||||
|
||||
// Service ID is now saved on the Rust side in handle_terminal_response
|
||||
|
||||
// Process any buffered input
|
||||
_processBufferedInputAsync().then((_) {
|
||||
notifyListeners();
|
||||
}).catchError((e) {
|
||||
debugPrint('[TerminalModel] Error processing buffered input: $e');
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
final persistentSessions =
|
||||
evt['persistent_sessions'] as List<dynamic>? ?? [];
|
||||
if (kWindowId != null && persistentSessions.isNotEmpty) {
|
||||
DesktopMultiWindow.invokeMethod(
|
||||
kWindowId!,
|
||||
kWindowEventRestoreTerminalSessions,
|
||||
jsonEncode({
|
||||
'persistent_sessions': persistentSessions,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
terminal.write('Failed to open terminal: $message\r\n');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processBufferedInputAsync() async {
|
||||
final buffer = List<String>.from(_inputBuffer);
|
||||
_inputBuffer.clear();
|
||||
|
||||
for (final data in buffer) {
|
||||
try {
|
||||
await bind.sessionSendTerminalInput(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
data: data,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error sending buffered input: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTerminalData(Map<String, dynamic> evt) {
|
||||
final data = evt['data'];
|
||||
|
||||
if (data != null) {
|
||||
try {
|
||||
String text = '';
|
||||
if (data is String) {
|
||||
// Try to decode as base64 first
|
||||
try {
|
||||
final bytes = base64Decode(data);
|
||||
text = utf8.decode(bytes);
|
||||
} catch (e) {
|
||||
// If base64 decode fails, treat as plain text
|
||||
text = data;
|
||||
}
|
||||
} else if (data is List) {
|
||||
// Handle if data comes as byte array
|
||||
text = utf8.decode(List<int>.from(data));
|
||||
} else {
|
||||
debugPrint('[TerminalModel] Unknown data type: ${data.runtimeType}');
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.write(text);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||
final int exitCode = evt['exit_code'] ?? 0;
|
||||
terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||
_terminalOpened = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||
final String message = evt['message'] ?? 'Unknown error';
|
||||
terminal.write('\r\nTerminal error: $message\r\n');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
// Terminal cleanup is handled server-side when service closes
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,10 @@ class UserModel {
|
||||
userName.value = user.name;
|
||||
isAdmin.value = user.isAdmin;
|
||||
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
|
||||
if (isWeb) {
|
||||
// ugly here, tmp solution
|
||||
bind.mainSetLocalOption(key: 'verifier', value: user.verifier ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
// update ab and group status
|
||||
@@ -184,7 +188,9 @@ class UserModel {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
if (loginResponse.user != null) {
|
||||
final isLogInDone = loginResponse.type == HttpType.kAuthResTypeToken &&
|
||||
loginResponse.access_token != null;
|
||||
if (isLogInDone && loginResponse.user != null) {
|
||||
_parseAndUpdateUser(loginResponse.user!);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ import 'dart:html';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_hbb/common/widgets/login.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/web/bridge.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
final List<StreamSubscription<MouseEvent>> mouseListeners = [];
|
||||
final List<StreamSubscription<KeyboardEvent>> keyListeners = [];
|
||||
@@ -49,14 +51,15 @@ class PlatformFFI {
|
||||
}
|
||||
|
||||
bool registerEventHandler(
|
||||
String eventName, String handlerName, HandleEvent handler) {
|
||||
String eventName, String handlerName, HandleEvent handler,
|
||||
{bool replace = false}) {
|
||||
debugPrint('registerEventHandler $eventName $handlerName');
|
||||
var handlers = _eventHandlers[eventName];
|
||||
if (handlers == null) {
|
||||
_eventHandlers[eventName] = {handlerName: handler};
|
||||
return true;
|
||||
} else {
|
||||
if (handlers.containsKey(handlerName)) {
|
||||
if (!replace && handlers.containsKey(handlerName)) {
|
||||
return false;
|
||||
} else {
|
||||
handlers[handlerName] = handler;
|
||||
@@ -112,6 +115,17 @@ class PlatformFFI {
|
||||
context["onInitFinished"] = () {
|
||||
completer.complete();
|
||||
};
|
||||
context['dialog'] = (type, title, text) {
|
||||
final uuid = Uuid();
|
||||
msgBox(SessionID(uuid.v4()), type, title, text, '', gFFI.dialogManager);
|
||||
};
|
||||
context['loginDialog'] = () {
|
||||
loginDialog();
|
||||
};
|
||||
context['closeConnection'] = () {
|
||||
gFFI.dialogManager.dismissAll();
|
||||
closeConnection();
|
||||
};
|
||||
context.callMethod('init');
|
||||
version = getByName('version');
|
||||
window.onContextMenu.listen((event) {
|
||||
|
||||
@@ -17,6 +17,7 @@ enum WindowType {
|
||||
FileTransfer,
|
||||
ViewCamera,
|
||||
PortForward,
|
||||
Terminal,
|
||||
Unknown
|
||||
}
|
||||
|
||||
@@ -33,6 +34,8 @@ extension Index on int {
|
||||
return WindowType.ViewCamera;
|
||||
case 4:
|
||||
return WindowType.PortForward;
|
||||
case 5:
|
||||
return WindowType.Terminal;
|
||||
default:
|
||||
return WindowType.Unknown;
|
||||
}
|
||||
@@ -61,6 +64,7 @@ class RustDeskMultiWindowManager {
|
||||
final List<int> _fileTransferWindows = List.empty(growable: true);
|
||||
final List<int> _viewCameraWindows = List.empty(growable: true);
|
||||
final List<int> _portForwardWindows = List.empty(growable: true);
|
||||
final List<int> _terminalWindows = List.empty(growable: true);
|
||||
|
||||
moveTabToNewWindow(int windowId, String peerId, String sessionId,
|
||||
WindowType windowType) async {
|
||||
@@ -343,6 +347,42 @@ class RustDeskMultiWindowManager {
|
||||
);
|
||||
}
|
||||
|
||||
Future<MultiWindowCallResult> newTerminal(
|
||||
String remoteId, {
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
}) async {
|
||||
// Iterate through terminal windows in reverse order to prioritize
|
||||
// the most recently added or used windows, as they are more likely
|
||||
// to have an active session.
|
||||
for (final windowId in _terminalWindows.reversed) {
|
||||
if (await DesktopMultiWindow.invokeMethod(
|
||||
windowId, kWindowEventActiveSession, remoteId)) {
|
||||
return MultiWindowCallResult(windowId, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal windows should always create new windows, not reuse
|
||||
// This avoids the MissingPluginException when trying to invoke
|
||||
// new_terminal on an inactive window
|
||||
var params = {
|
||||
"type": WindowType.Terminal.index,
|
||||
"id": remoteId,
|
||||
"password": password,
|
||||
"forceRelay": forceRelay,
|
||||
"isSharedPassword": isSharedPassword,
|
||||
"connToken": connToken,
|
||||
};
|
||||
final msg = jsonEncode(params);
|
||||
|
||||
// Always create a new window for terminal
|
||||
final windowId = await newSessionWindow(
|
||||
WindowType.Terminal, remoteId, msg, _terminalWindows, false);
|
||||
return MultiWindowCallResult(windowId, null);
|
||||
}
|
||||
|
||||
Future<MultiWindowCallResult> call(
|
||||
WindowType type, String methodName, dynamic args) async {
|
||||
final wnds = _findWindowsByType(type);
|
||||
@@ -373,6 +413,8 @@ class RustDeskMultiWindowManager {
|
||||
return _viewCameraWindows;
|
||||
case WindowType.PortForward:
|
||||
return _portForwardWindows;
|
||||
case WindowType.Terminal:
|
||||
return _terminalWindows;
|
||||
case WindowType.Unknown:
|
||||
break;
|
||||
}
|
||||
@@ -395,6 +437,8 @@ class RustDeskMultiWindowManager {
|
||||
case WindowType.PortForward:
|
||||
_portForwardWindows.clear();
|
||||
break;
|
||||
case WindowType.Terminal:
|
||||
_terminalWindows.clear();
|
||||
case WindowType.Unknown:
|
||||
break;
|
||||
}
|
||||
@@ -426,9 +470,13 @@ class RustDeskMultiWindowManager {
|
||||
if (windows.isEmpty) {
|
||||
return;
|
||||
}
|
||||
for (final wId in windows) {
|
||||
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
|
||||
await saveWindowPosition(type, windowId: wId);
|
||||
for (int i = 0; i < windows.length; i++) {
|
||||
final wId = windows[i];
|
||||
final shouldSavePos = type != WindowType.Terminal || i == windows.length - 1;
|
||||
if (shouldSavePos) {
|
||||
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
|
||||
await saveWindowPosition(type, windowId: wId);
|
||||
}
|
||||
try {
|
||||
await WindowController.fromWindowId(wId).setPreventClose(false);
|
||||
await WindowController.fromWindowId(wId).close();
|
||||
|
||||
@@ -81,6 +81,7 @@ class RustdeskImpl {
|
||||
required bool isViewCamera,
|
||||
required bool isPortForward,
|
||||
required bool isRdp,
|
||||
required bool isTerminal,
|
||||
required String switchUuid,
|
||||
required bool forceRelay,
|
||||
required String password,
|
||||
@@ -94,7 +95,8 @@ class RustdeskImpl {
|
||||
'password': password,
|
||||
'is_shared_password': isSharedPassword,
|
||||
'isFileTransfer': isFileTransfer,
|
||||
'isViewCamera': isViewCamera
|
||||
'isViewCamera': isViewCamera,
|
||||
'isTerminal': isTerminal
|
||||
})
|
||||
]);
|
||||
}
|
||||
@@ -267,6 +269,16 @@ class RustdeskImpl {
|
||||
]));
|
||||
}
|
||||
|
||||
Future<int?> sessionGetTrackpadSpeed(
|
||||
{required UuidValue sessionId, dynamic hint}) {
|
||||
throw UnimplementedError("sessionGetTrackpadSpeed");
|
||||
}
|
||||
|
||||
Future<void> sessionSetTrackpadSpeed(
|
||||
{required UuidValue sessionId, required int value, dynamic hint}) {
|
||||
throw UnimplementedError("sessionSetTrackpadSpeed");
|
||||
}
|
||||
|
||||
Future<String?> sessionGetScrollStyle(
|
||||
{required UuidValue sessionId, dynamic hint}) {
|
||||
return Future(() =>
|
||||
@@ -896,8 +908,18 @@ class RustdeskImpl {
|
||||
return js.context.callMethod('getByName', ['option:local', key]);
|
||||
}
|
||||
|
||||
// Do not return the real environment variables.
|
||||
// Use the global variable as the environment variable in web.
|
||||
String mainGetEnv({required String key, dynamic hint}) {
|
||||
throw UnimplementedError("mainGetEnv");
|
||||
return js.context.callMethod('getByName', ['envvar', key]);
|
||||
}
|
||||
|
||||
// Use the global variable as the environment variable in web.
|
||||
void mainSetEnv({required String key, String? value, dynamic hint}) {
|
||||
js.context.callMethod('setByName', [
|
||||
'envvar',
|
||||
jsonEncode({'name': key, 'value': value})
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> mainSetLocalOption(
|
||||
@@ -1520,15 +1542,20 @@ class RustdeskImpl {
|
||||
|
||||
Future<void> mainAccountAuth(
|
||||
{required String op, required bool rememberMe, dynamic hint}) {
|
||||
throw UnimplementedError("mainAccountAuth");
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'account_auth',
|
||||
jsonEncode({'op': op, 'remember': rememberMe})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> mainAccountAuthCancel({dynamic hint}) {
|
||||
throw UnimplementedError("mainAccountAuthCancel");
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ['account_auth_cancel']));
|
||||
}
|
||||
|
||||
Future<String> mainAccountAuthResult({dynamic hint}) {
|
||||
throw UnimplementedError("mainAccountAuthResult");
|
||||
return Future(
|
||||
() => js.context.callMethod('getByName', ['account_auth_result']));
|
||||
}
|
||||
|
||||
Future<void> mainOnMainWindowClose({dynamic hint}) {
|
||||
@@ -1852,5 +1879,105 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("sessionGetConnToken");
|
||||
}
|
||||
|
||||
String mainGetPrinterNames({dynamic hint}) {
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<void> sessionPrinterResponse(
|
||||
{required UuidValue sessionId,
|
||||
required int id,
|
||||
required String path,
|
||||
required String printerName,
|
||||
dynamic hint}) {
|
||||
throw UnimplementedError("sessionPrinterResponse");
|
||||
}
|
||||
|
||||
Future<String> mainGetCommon({required String key, dynamic hint}) {
|
||||
throw UnimplementedError("mainGetCommon");
|
||||
}
|
||||
|
||||
String mainGetCommonSync({required String key, dynamic hint}) {
|
||||
throw UnimplementedError("mainGetCommonSync");
|
||||
}
|
||||
|
||||
Future<void> mainSetCommon(
|
||||
{required String key, required String value, dynamic hint}) {
|
||||
throw UnimplementedError("mainSetCommon");
|
||||
}
|
||||
|
||||
Future<String> sessionHandleScreenshot(
|
||||
{required UuidValue sessionId, required String action, dynamic hint}) {
|
||||
throw UnimplementedError("sessionHandleScreenshot");
|
||||
}
|
||||
|
||||
String? sessionGetCommonSync(
|
||||
{required UuidValue sessionId,
|
||||
required String key,
|
||||
required String param,
|
||||
dynamic hint}) {
|
||||
throw UnimplementedError("sessionGetCommonSync");
|
||||
}
|
||||
|
||||
Future<void> sessionTakeScreenshot(
|
||||
{required UuidValue sessionId, required int display, dynamic hint}) {
|
||||
throw UnimplementedError("sessionTakeScreenshot");
|
||||
}
|
||||
|
||||
Future<void> sessionOpenTerminal(
|
||||
{required UuidValue sessionId,
|
||||
required int terminalId,
|
||||
required int rows,
|
||||
required int cols,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'open_terminal',
|
||||
jsonEncode({
|
||||
'terminal_id': terminalId,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionSendTerminalInput(
|
||||
{required UuidValue sessionId,
|
||||
required int terminalId,
|
||||
required String data,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'send_terminal_input',
|
||||
jsonEncode({
|
||||
'terminal_id': terminalId,
|
||||
'data': data,
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionResizeTerminal(
|
||||
{required UuidValue sessionId,
|
||||
required int terminalId,
|
||||
required int rows,
|
||||
required int cols,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'resize_terminal',
|
||||
jsonEncode({
|
||||
'terminal_id': terminalId,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionCloseTerminal(
|
||||
{required UuidValue sessionId, required int terminalId, dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'close_terminal',
|
||||
jsonEncode({
|
||||
'terminal_id': terminalId,
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ PODS:
|
||||
- flutter_custom_cursor (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- FMDB (2.7.12):
|
||||
- FMDB/standard (= 2.7.12)
|
||||
- FMDB/Core (2.7.12)
|
||||
- FMDB/standard (2.7.12):
|
||||
- FMDB/Core
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- path_provider_foundation (0.0.1):
|
||||
@@ -17,9 +22,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- screen_retriever (0.0.1):
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- sqflite (0.0.2):
|
||||
- FlutterMacOS
|
||||
- FMDB (>= 2.7.5)
|
||||
- texture_rgba_renderer (0.0.1):
|
||||
- FlutterMacOS
|
||||
- uni_links_desktop (0.0.1):
|
||||
@@ -46,7 +51,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
|
||||
- texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`)
|
||||
- uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
@@ -55,6 +60,10 @@ DEPENDENCIES:
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
desktop_drop:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||
@@ -75,7 +84,7 @@ EXTERNAL SOURCES:
|
||||
screen_retriever:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
|
||||
texture_rgba_renderer:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos
|
||||
uni_links_desktop:
|
||||
@@ -92,24 +101,25 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
|
||||
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
|
||||
desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43
|
||||
desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a
|
||||
device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5
|
||||
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
|
||||
flutter_custom_cursor: 37e588711a2746f5cf48adb58b582cacff11c0c6
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
|
||||
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||
FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6
|
||||
package_info_plus: 122abb51244f66eead59ce7c9c200d6b53111779
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936
|
||||
sqflite: c73556b2499b92f0b6e6946abe4a4084510cdf90
|
||||
texture_rgba_renderer: 6661f577ea5d4990e964c7e3840e544ac798e6da
|
||||
uni_links_desktop: 34322c2646e4c9abc69b62e1865f9782d2850ba2
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
|
||||
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
|
||||
window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9
|
||||
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.3.9+57
|
||||
version: 1.4.1+59
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
@@ -106,6 +106,9 @@ dependencies:
|
||||
device_info_plus: ^9.1.0
|
||||
qr_flutter: ^4.1.0
|
||||
extended_text: 14.0.0
|
||||
xterm: 4.0.0
|
||||
sqflite: 2.2.0
|
||||
google_fonts: ^6.2.1
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
@@ -118,7 +121,8 @@ dev_dependencies:
|
||||
|
||||
dependency_overrides:
|
||||
intl: ^0.19.0
|
||||
|
||||
flutter_plugin_android_lifecycle: 2.0.17
|
||||
|
||||
# rerun: flutter pub run flutter_launcher_icons
|
||||
flutter_icons:
|
||||
image_path: "../res/icon.png"
|
||||
@@ -193,4 +197,3 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user