Compare commits
517 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23754630e8 | ||
|
|
8e6e91eb4a | ||
|
|
9cfa551163 | ||
|
|
5b21441898 | ||
|
|
4ed8696d1d | ||
|
|
ae06f27372 | ||
|
|
33e1493932 | ||
|
|
22b1dcaf7b | ||
|
|
426a68775f | ||
|
|
3c0be4e40e | ||
|
|
3787b45b49 | ||
|
|
7d06de00fb | ||
|
|
6f8af9d114 | ||
|
|
0a672f092a | ||
|
|
ef62f1db29 | ||
|
|
7f804a0e45 | ||
|
|
b2dff336ce | ||
|
|
a6571e71e4 | ||
|
|
81f711eb00 | ||
|
|
c8a8e06558 | ||
|
|
2c079f53a9 | ||
|
|
322ffe288e | ||
|
|
c340eb0e57 | ||
|
|
4e953291ed | ||
|
|
1dea5fee0e | ||
|
|
9f24b46fee | ||
|
|
0808c41a1c | ||
|
|
296c6df462 | ||
|
|
13ee3e907d | ||
|
|
ce7d794b4c | ||
|
|
fb10069632 | ||
|
|
43a7677644 | ||
|
|
58fa32d7ea | ||
|
|
934d6c3987 | ||
|
|
2d7c6ef21f | ||
|
|
99a97e6a6c | ||
|
|
017a10e8c8 | ||
|
|
41ffa8ba08 | ||
|
|
e029d00cfa | ||
|
|
268534d5e7 | ||
|
|
a7d2bc63f9 | ||
|
|
559115c43c | ||
|
|
1277c7d60c | ||
|
|
9b69c7e972 | ||
|
|
a903f710ea | ||
|
|
b75f4daa47 | ||
|
|
fef44ffa57 | ||
|
|
5a812e3b2f | ||
|
|
910dcf2036 | ||
|
|
44a28aa5bd | ||
|
|
f7f947beb9 | ||
|
|
d03a9e2baf | ||
|
|
ca22316e95 | ||
|
|
ef99c479aa | ||
|
|
fa9260c763 | ||
|
|
fab11c8ffa | ||
|
|
9bd9658a92 | ||
|
|
213880c14d | ||
|
|
0550397046 | ||
|
|
f7a5a506f6 | ||
|
|
0f34c50bd2 | ||
|
|
055826e26f | ||
|
|
a30582c840 | ||
|
|
d106d97b99 | ||
|
|
d4410e78e2 | ||
|
|
e3fcc6cce3 | ||
|
|
265d08fc3b | ||
|
|
7c8329c5c6 | ||
|
|
d3947c9a19 | ||
|
|
cd99331668 | ||
|
|
472e18b10a | ||
|
|
d443f5de28 | ||
|
|
3242d132f6 | ||
|
|
e66d2facd4 | ||
|
|
f15b8dc0da | ||
|
|
3275824aec | ||
|
|
965cb704ec | ||
|
|
938e165470 | ||
|
|
9058ef3344 | ||
|
|
ed39cc3038 | ||
|
|
a77752c4cb | ||
|
|
c9940957f0 | ||
|
|
c90d72d720 | ||
|
|
2c30bd9d24 | ||
|
|
f2dc8e21a8 | ||
|
|
6a0da9cf09 | ||
|
|
d55974c352 | ||
|
|
a898c22f4b | ||
|
|
b82e8bedfc | ||
|
|
7453cefd94 | ||
|
|
1ed6b958cb | ||
|
|
57896ab176 | ||
|
|
5c370b3914 | ||
|
|
182e35adc7 | ||
|
|
d0a360fd80 | ||
|
|
2fbc0625de | ||
|
|
d3d20a4e20 | ||
|
|
2c088d3504 | ||
|
|
6f9728f2d4 | ||
|
|
30552fd202 | ||
|
|
9826c4e943 | ||
|
|
bb9445bd0f | ||
|
|
1f7e66f4cb | ||
|
|
2a34e918a0 | ||
|
|
21c0d924ab | ||
|
|
c8d5ee6565 | ||
|
|
3d8fc7ca7b | ||
|
|
246b5b93f8 | ||
|
|
2183c0980b | ||
|
|
4ae301710d | ||
|
|
5f9390c210 | ||
|
|
0f3a03aab7 | ||
|
|
02f455b0cc | ||
|
|
ffddf60184 | ||
|
|
482840b8bb | ||
|
|
a3637cf2b6 | ||
|
|
48669cdb34 | ||
|
|
a953845ba7 | ||
|
|
8d71534839 | ||
|
|
d110118961 | ||
|
|
fa1ed2bc0c | ||
|
|
3f28978dad | ||
|
|
02cd121465 | ||
|
|
5481c300b2 | ||
|
|
7b75257a4a | ||
|
|
c02e5cad73 | ||
|
|
dee03c0f9f | ||
|
|
d1159764f6 | ||
|
|
eacb07988d | ||
|
|
a375766ac2 | ||
|
|
9b9276e752 | ||
|
|
753a2ab2b7 | ||
|
|
0cef5f79ee | ||
|
|
b11a8dfe54 | ||
|
|
2d1c94f1ef | ||
|
|
e14e850e10 | ||
|
|
3176391693 | ||
|
|
5277300943 | ||
|
|
878e1ff290 | ||
|
|
8d453010a4 | ||
|
|
e2f6030590 | ||
|
|
bf3f8706f8 | ||
|
|
5c9b4abab2 | ||
|
|
9fb4862a45 | ||
|
|
65df6897a6 | ||
|
|
529810f2f4 | ||
|
|
df0ff4f134 | ||
|
|
6c949a9602 | ||
|
|
f933f46283 | ||
|
|
4080907d2b | ||
|
|
ed5cd21cb6 | ||
|
|
aa8278e1d5 | ||
|
|
0f526fce6c | ||
|
|
15d471e520 | ||
|
|
c47e94813d | ||
|
|
c979cbcac7 | ||
|
|
6b2a1dfd84 | ||
|
|
7948d3144a | ||
|
|
d499098c4f | ||
|
|
42be442385 | ||
|
|
e2ec6a5be8 | ||
|
|
438cef8cf9 | ||
|
|
7bacf7cdc9 | ||
|
|
c5e76972aa | ||
|
|
7ca8e0d437 | ||
|
|
a98852e279 | ||
|
|
d0e9c6dc57 | ||
|
|
ac70f380a6 | ||
|
|
34cf9d6181 | ||
|
|
db4296533a | ||
|
|
6381f43f01 | ||
|
|
f4fb31d7a1 | ||
|
|
9e22f9639a | ||
|
|
9b854d3034 | ||
|
|
2c88a44a53 | ||
|
|
0c2b86c8e7 | ||
|
|
74752bbd2f | ||
|
|
ad396b4155 | ||
|
|
5ff1740b5b | ||
|
|
e0ab3f0c92 | ||
|
|
9b77e91d79 | ||
|
|
d187121645 | ||
|
|
a22f2108c6 | ||
|
|
bf24869c6a | ||
|
|
4e9a370ff6 | ||
|
|
1aed6f3c2e | ||
|
|
6367d50d76 | ||
|
|
f33ed27419 | ||
|
|
870c8cb158 | ||
|
|
0b9d7925b5 | ||
|
|
16b625f8b4 | ||
|
|
16d301a783 | ||
|
|
212bbaf44c | ||
|
|
1d6037003a | ||
|
|
6f4b23b40b | ||
|
|
4e82766ba4 | ||
|
|
dc86db5206 | ||
|
|
5a75ea723b | ||
|
|
d59f216c26 | ||
|
|
160edcc1cd | ||
|
|
59d597de8a | ||
|
|
806351b6c1 | ||
|
|
e7909a0dbd | ||
|
|
d6d44be1b7 | ||
|
|
53efaf125c | ||
|
|
1fb0123ed7 | ||
|
|
a0659a277a | ||
|
|
77064cc2f8 | ||
|
|
1954790808 | ||
|
|
4263643200 | ||
|
|
43ec57c769 | ||
|
|
302dad2016 | ||
|
|
fdb8b498cb | ||
|
|
f6af59b044 | ||
|
|
ad1ed132d1 | ||
|
|
466d456760 | ||
|
|
6bc3b38b56 | ||
|
|
39b91911cb | ||
|
|
e85989e9d9 | ||
|
|
e7f672899b | ||
|
|
9538eba64e | ||
|
|
b37b271fce | ||
|
|
77be752ff1 | ||
|
|
725a47268e | ||
|
|
2ba215a6d7 | ||
|
|
6533a1b98d | ||
|
|
1f2f5a41d4 | ||
|
|
4e7680e322 | ||
|
|
f32591c3d1 | ||
|
|
6ec217263d | ||
|
|
8899b90725 | ||
|
|
7ece7e730a | ||
|
|
d55b98b187 | ||
|
|
d9674a2d77 | ||
|
|
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: "120deac3062162151622ca4860575a33844ba10b"
|
||||
|
||||
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-24.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 \
|
||||
|
||||
255
.github/workflows/flutter-build.yml
vendored
255
.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"
|
||||
@@ -31,9 +31,15 @@ env:
|
||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2025.01.13
|
||||
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
|
||||
VERSION: "1.3.9"
|
||||
# vcpkg version: 2025.08.27
|
||||
# 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: "120deac3062162151622ca4860575a33844ba10b"
|
||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||
VERSION: "1.4.4"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -172,24 +178,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."
|
||||
}
|
||||
@@ -386,6 +392,13 @@ jobs:
|
||||
ls -l ./libs/portable/Runner.res;
|
||||
fi
|
||||
|
||||
- name: Upload unsigned
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
|
||||
path: Release
|
||||
|
||||
- name: Sign rustdesk files
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
||||
shell: bash
|
||||
@@ -419,80 +432,6 @@ jobs:
|
||||
files: |
|
||||
./SignOutput/rustdesk-*.exe
|
||||
|
||||
build-for-macOS-arm64-selfhost:
|
||||
# use build-for-macOS instead
|
||||
if: false
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
needs: [generate-bridge]
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Build rustdesk
|
||||
run: |
|
||||
./build.py --flutter --hwcodec --unix-file-copy-paste
|
||||
|
||||
- name: create unsigned dmg
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
run: |
|
||||
CREATE_DMG="$(command -v create-dmg)"
|
||||
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-arm64.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||
|
||||
- name: Upload unsigned macOS app
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: rustdesk-unsigned-macos-arm64
|
||||
path: rustdesk-${{ env.VERSION }}-arm64.dmg # can not upload the directory directly or tar.gz file, which destroy the link structure, causing the codesign failed
|
||||
|
||||
- name: Codesign app and create signed dmg
|
||||
if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true'
|
||||
run: |
|
||||
# Patch create-dmg to give more attempts to unmount image
|
||||
CREATE_DMG="$(command -v create-dmg)"
|
||||
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||
# start sign the rustdesk.app and dmg
|
||||
rm -rf *.dmg || true
|
||||
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
|
||||
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
|
||||
# notarize the rustdesk-${{ env.VERSION }}.dmg
|
||||
rcodesign notary-submit --api-key-path ~/.p12/api-key.json --staple rustdesk-${{ env.VERSION }}.dmg
|
||||
|
||||
- name: Rename rustdesk
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
run: |
|
||||
for name in rustdesk*??.dmg; do
|
||||
mv "$name" "${name%%.dmg}-aarch64.dmg"
|
||||
done
|
||||
|
||||
- name: Publish DMG package
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
rustdesk*-aarch64.dmg
|
||||
|
||||
build-rustdesk-ios:
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
name: build rustdesk ios ipa
|
||||
@@ -612,63 +551,6 @@ jobs:
|
||||
# files: |
|
||||
# flutter/build/ios/ipa/*.ipa
|
||||
|
||||
build-rustdesk-ios-selfhost:
|
||||
#if: ${{ inputs.upload-artifact }}
|
||||
if: false
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
needs: [generate-bridge]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Build rustdesk lib
|
||||
run: |
|
||||
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||
|
||||
- name: Build rustdesk
|
||||
# ios sdk not installed on this machine, I will install it later after I am back home
|
||||
if: false
|
||||
shell: bash
|
||||
run: |
|
||||
pushd flutter
|
||||
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign
|
||||
# for easy debugging
|
||||
flutter build ipa --release --no-codesign
|
||||
|
||||
# - name: Upload Artifacts
|
||||
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||
# uses: actions/upload-artifact@master
|
||||
# with:
|
||||
# name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||
# path: flutter/build/ios/ipa/*.ipa
|
||||
|
||||
# - name: Publish ipa package
|
||||
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||
# uses: softprops/action-gh-release@v1
|
||||
# with:
|
||||
# prerelease: true
|
||||
# tag_name: ${{ env.TAG_NAME }}
|
||||
# files: |
|
||||
# flutter/build/ios/ipa/*.ipa
|
||||
|
||||
build-for-macOS:
|
||||
name: ${{ matrix.job.target }}
|
||||
@@ -687,7 +569,7 @@ jobs:
|
||||
}
|
||||
- {
|
||||
target: aarch64-apple-darwin,
|
||||
os: macos-latest,
|
||||
os: macos-14,
|
||||
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
|
||||
extra-build-args: "--screencapturekit",
|
||||
arch: aarch64,
|
||||
@@ -741,7 +623,7 @@ jobs:
|
||||
|
||||
- name: Install build runtime
|
||||
run: |
|
||||
brew install llvm create-dmg nasm cmake gcc wget ninja
|
||||
brew install llvm create-dmg nasm
|
||||
# pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner
|
||||
if command -v pkg-config &>/dev/null; then
|
||||
echo "pkg-config is already installed"
|
||||
@@ -881,6 +763,7 @@ jobs:
|
||||
needs:
|
||||
- build-for-macOS
|
||||
- build-for-windows-flutter
|
||||
- build-for-windows-sciter
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
steps:
|
||||
@@ -902,9 +785,15 @@ jobs:
|
||||
name: rustdesk-unsigned-windows-x86_64
|
||||
path: ./windows-x86_64/
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: rustdesk-unsigned-windows-x86
|
||||
path: ./windows-x86/
|
||||
|
||||
- name: Combine unsigned app
|
||||
run: |
|
||||
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64
|
||||
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
|
||||
|
||||
- name: Publish unsigned app
|
||||
uses: softprops/action-gh-release@v1
|
||||
@@ -924,21 +813,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 +864,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 +877,7 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-17-jdk-headless \
|
||||
@@ -1111,6 +1001,8 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||
# Increase Gradle JVM memory for CI builds
|
||||
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
case ${{ matrix.job.target }} in
|
||||
@@ -1206,7 +1098,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 +1136,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 +1149,7 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-17-jdk-headless \
|
||||
@@ -1317,6 +1210,8 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
|
||||
# Increase Gradle JVM memory for CI builds
|
||||
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||
@@ -1396,7 +1291,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,
|
||||
}
|
||||
@@ -1552,7 +1447,8 @@ jobs:
|
||||
rpm \
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils
|
||||
xz-utils \
|
||||
libssl-dev
|
||||
# we have libopus compiled by us.
|
||||
apt-get remove -y libopus-dev || true
|
||||
# output devs
|
||||
@@ -1731,7 +1627,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,
|
||||
@@ -1761,6 +1657,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Modify vcpkg.json for armv7
|
||||
if: matrix.job.vcpkg-triplet == 'arm-linux'
|
||||
run: |
|
||||
# Replace the baseline in vcpkg.json with ARMV7_VCPKG_COMMIT_ID for armv7 builds
|
||||
sed -i 's/"baseline": ".*"/"baseline": "${{ env.ARMV7_VCPKG_COMMIT_ID }}"/' vcpkg.json
|
||||
echo "Modified vcpkg.json for armv7 build:"
|
||||
grep -A 2 -B 2 '"baseline"' vcpkg.json
|
||||
|
||||
- name: Free Space
|
||||
run: |
|
||||
df -h
|
||||
@@ -1824,12 +1728,13 @@ jobs:
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils \
|
||||
zip
|
||||
zip \
|
||||
libssl-dev
|
||||
# arm-linux needs CMake and vcokg built from source as there
|
||||
# are no prebuilts available from Kitware and Microsoft
|
||||
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
|
||||
# install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake
|
||||
apt-get install -y gcc-8 g++-8 libssl-dev
|
||||
apt-get install -y gcc-8 g++-8
|
||||
# bootstrap CMake amd add it to PATH
|
||||
git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake
|
||||
pushd /tmp/cmake
|
||||
@@ -1846,11 +1751,12 @@ jobs:
|
||||
rm -rf vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
pushd vcpkg
|
||||
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
|
||||
# build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux
|
||||
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
|
||||
git reset --hard ${{ env.ARMV7_VCPKG_COMMIT_ID }}
|
||||
CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics
|
||||
else
|
||||
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
|
||||
sh bootstrap-vcpkg.sh -disableMetrics
|
||||
fi
|
||||
popd
|
||||
@@ -1939,7 +1845,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 +1874,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 +1905,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 +1980,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:
|
||||
|
||||
14
.github/workflows/playground.yml
vendored
14
.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: "120deac3062162151622ca4860575a33844ba10b"
|
||||
VERSION: "1.4.4"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -127,7 +126,7 @@ jobs:
|
||||
|
||||
- name: Install build runtime
|
||||
run: |
|
||||
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
|
||||
brew install llvm create-dmg nasm pkg-config
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
@@ -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.4"
|
||||
release-tag: "1.4.4"
|
||||
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
|
||||
3518
Cargo.lock
generated
3518
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
43
Cargo.toml
43
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.3.9"
|
||||
version = "1.4.4"
|
||||
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,6 +81,9 @@ 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"}
|
||||
reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
|
||||
@@ -95,9 +97,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 +121,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]
|
||||
@@ -136,6 +146,10 @@ core-graphics = "0.22"
|
||||
include_dir = "0.7"
|
||||
fruitbasket = "0.10"
|
||||
objc_id = "0.1"
|
||||
# If we use piet "0.7" here, we must also update core-graphics to "0.24".
|
||||
piet = "0.6"
|
||||
piet-coregraphics = "0.6"
|
||||
foreign-types = "0.3"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
|
||||
@@ -147,13 +161,11 @@ keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
tiny-skia = "0.11"
|
||||
softbuffer = "0.4"
|
||||
fontdb = "0.23"
|
||||
bytemuck = "1.23"
|
||||
ttf-parser = "0.25"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
@@ -172,6 +184,11 @@ once_cell = {version = "1.18", optional = true}
|
||||
nix = { version = "0.29", features = ["term", "process"]}
|
||||
gtk = "0.18"
|
||||
termios = "0.3"
|
||||
terminfo = "0.8"
|
||||
winit = "0.30"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
@@ -215,7 +232,3 @@ panic = 'abort'
|
||||
strip = true
|
||||
#opt-level = 'z' # only have smaller size after strip
|
||||
rpath = true
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = '...' # Platform-specific.
|
||||
#strip = "debuginfo"
|
||||
|
||||
16
README.md
16
README.md
@@ -4,7 +4,7 @@
|
||||
<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>] | [<a href="docs/README-NO.md">Norsk</a>]<br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</a>]<br>
|
||||
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
|
||||
</p>
|
||||
|
||||
@@ -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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
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.4
|
||||
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.4
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -102,3 +102,4 @@ AppDir:
|
||||
AppImage:
|
||||
arch: x86_64
|
||||
update-information: guess
|
||||
comp: gzip
|
||||
|
||||
9
build.rs
9
build.rs
@@ -18,7 +18,7 @@ fn build_mac() {
|
||||
b.flag("-DNO_InputMonitoringAuthStatus=1");
|
||||
}
|
||||
}
|
||||
b.file(file).compile("macos");
|
||||
b.flag("-std=c++17").file(file).compile("macos");
|
||||
println!("cargo:rerun-if-changed={}", file);
|
||||
}
|
||||
|
||||
@@ -68,11 +68,8 @@ fn install_android_deps() {
|
||||
}
|
||||
path.push(target);
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"cargo:rustc-link-search={}",
|
||||
path.join("lib").to_str().unwrap()
|
||||
)
|
||||
"cargo:rustc-link-search={}",
|
||||
path.join("lib").to_str().unwrap()
|
||||
);
|
||||
println!("cargo:rustc-link-lib=ndk_compat");
|
||||
println!("cargo:rustc-link-lib=oboe");
|
||||
|
||||
137
docs/CODE_OF_CONDUCT-DE.md
Normal file
137
docs/CODE_OF_CONDUCT-DE.md
Normal file
@@ -0,0 +1,137 @@
|
||||
|
||||
# Verhaltenskodex (Code of Conduct) für Mitwirkende
|
||||
|
||||
## Unsere Verpflichtung
|
||||
|
||||
Wir als Mitglieder, Mitwirkende und Führungskräfte verpflichten uns,
|
||||
die Teilnahme unserer Community zu einer Erfahrung zu machen,
|
||||
die für alle frei von Belästigungen ist, unabhängig von Alter, Körpergröße,
|
||||
sichtbarer oder unsichtbarer Behinderung, ethnischer Zugehörigkeit,
|
||||
Geschlechtsmerkmalen, Geschlechtsidentität und -ausdruck, Erfahrungsniveau,
|
||||
Bildung, sozioökonomischem Status, Nationalität, persönlichem Erscheinungsbild,
|
||||
Rasse, Religion oder sexueller Identität und Orientierung.
|
||||
|
||||
Wir verpflichten uns, so zu handeln und zu interagieren, dass wir zu einer offenen,
|
||||
einladenden, vielfältigen, integrativen und lebendigen Gemeinschaft beitragen.
|
||||
|
||||
## Unsere Standards
|
||||
|
||||
Beispiele für Verhaltensweisen, die zu einem positiven Umfeld für unsere
|
||||
Gemeinschaft beitragen, sind:
|
||||
|
||||
* Empathie und Freundlichkeit gegenüber anderen Menschen zu zeigen
|
||||
* Respektvoll gegenüber anderen Meinungen, Sichtweisen und Erfahrungen zu sein
|
||||
* Das Vergeben von sowie das großzügige Empfangen von konstruktivem Feedback
|
||||
* Verantwortung übernehmen, sich bei den Betroffenen entschuldigen
|
||||
und aus den Erfahrungen lernen
|
||||
* Nicht darauf zu achten, was das Beste für sich selbst,
|
||||
sondern zu Achten, was das Beste für die gesamte Community ist
|
||||
|
||||
Beispiele für nicht akzeptables Verhalten sind:
|
||||
|
||||
* Die Verwendung sexualisierter bzw. anstößiger Sprache oder Bilder
|
||||
sowie sexuelle Aufmerksamkeit oder Annäherungsversuche jeglicher Art
|
||||
* Trolling, beleidigende oder herabwürdigende Kommentare
|
||||
sowie persönliche oder politische Angriffe
|
||||
* Öffentliche sowie private Belästigung
|
||||
* Das Teilen privater Informationen anderer Leute ohne deren explizite Zustimmung,
|
||||
wie bspw. die physische oder die E-Mail-Adresse
|
||||
* Anderes Verhalten, das in einem professionellen Umfeld begründeter Weise als
|
||||
unangemessen angesehen werden könnte
|
||||
|
||||
## Durchsetzungsbefugnisse
|
||||
|
||||
Die Leiter der Community sind dafür verantwortlich, unsere Standards für
|
||||
akzeptables Verhalten zu klären und durchzusetzen und werden angemessene
|
||||
und faire Korrekturmaßnahmen ergreifen, wenn sie ein Verhalten als unangemessen,
|
||||
bedrohlich, beleidigend oder schädlich erachten.
|
||||
|
||||
Die Leiter der Community haben das Recht und die Pflicht, Kommentare, Commits,
|
||||
Code, Wiki-Bearbeitungen, Issues und andere Beiträge, die nicht mit dem
|
||||
Verhaltenskodex vereinbar sind, zu entfernen, zu bearbeiten oder abzulehnen.
|
||||
Sie werden, falls angebracht, die Gründe für Moderationsentscheidungen mitteilen.
|
||||
|
||||
## Geltungsbereich
|
||||
|
||||
Dieser Verhaltenskodex gilt in allen Community-Bereichen und auch dann, wenn
|
||||
eine Person die Community offiziell in öffentlichen Bereichen vertritt.
|
||||
Beispiele für die Vertretung unserer Community sind die Verwendung einer
|
||||
offiziellen E-Mail-Adresse, das Posten über einen offiziellen
|
||||
Social-Media-Account oder die Tätigkeit als ernannter
|
||||
Vertreter bei einer Online- oder Präsenzveranstaltung.
|
||||
|
||||
## Geltendmachung
|
||||
|
||||
Fälle von missbräuchlichem, belästigendem oder anderweitig inakzeptablem Verhalten können
|
||||
den für die Durchsetzung zuständigen Community-Leitern
|
||||
unter [info@rustdesk.com](mailto:info@rustdesk.com) gemeldet werden.
|
||||
Jeder Fall wird umgehend und fair geprüft und untersucht.
|
||||
|
||||
## Richtlinien zur Geltendmachung
|
||||
|
||||
Die Community-Leiter werden die folgenden Community-Auswirkungsrichtlinien befolgen,
|
||||
um die Konsequenzen für jede Handlung zu bestimmen, die sie als Verstoß gegen diesen
|
||||
Verhaltenskodex ansehen:
|
||||
|
||||
### 1. Korrektur
|
||||
|
||||
**Auswirkungen auf die Community**: Verwendung unangemessener Sprache oder anderes
|
||||
Verhalten, welches als unprofessionell oder in der Community unerwünscht angesehen wird.
|
||||
|
||||
**Konsequenz**: Eine private, schriftliche Verwarnung durch die Leiter der Community,
|
||||
in der die Art des Verstoßes klar dargelegt und erklärt wird, warum das
|
||||
Verhalten unangemessen war. Eine öffentliche Entschuldigung kann verlangt werden.
|
||||
|
||||
### 2. Warnung
|
||||
|
||||
**Auswirkungen auf die Community**: Ein Verstoß durch einen einzelnen Vorfall
|
||||
oder eine Reihe von Handlungen.
|
||||
|
||||
**Konsequenz**: Eine Verwarnung mit Konsequenzen für das weitere Verhalten. Keine
|
||||
Interaktion mit den beteiligten Personen, einschließlich unaufgeforderter Interaktion mit
|
||||
denjenigen, die den Verhaltenskodex durchsetzen, für einen bestimmten Zeitraum. Dies
|
||||
schließt die Vermeidung von Interaktionen in Gemeinschaftsräumen sowie externen Kanälen
|
||||
wie sozialen Medien ein. Ein Verstoß gegen diese Bedingungen kann zu einer vorübergehenden oder
|
||||
dauerhaften Sperrung führen.
|
||||
|
||||
### 3. Temporärer Sperrung
|
||||
|
||||
|
||||
**Auswirkungen auf die Community**: Ein schwerwiegender Verstoß gegen die Community-Standards,
|
||||
einschließlich anhaltend unangemessenem Verhalten.
|
||||
|
||||
**Konsequenz**: Eine vorübergehende Sperrung jeglicher Art von Interaktion oder öffentlicher
|
||||
Kommunikation mit der Community für einen bestimmten Zeitraum. Während dieses Zeitraums sind
|
||||
keine öffentlichen oder privaten Interaktionen mit den betroffenen Personen,
|
||||
einschließlich unaufgeforderter Interaktionen mit denjenigen,
|
||||
die den Verhaltenskodex durchsetzen, erlaubt.
|
||||
Ein Verstoß gegen diese Bedingungen kann zu einer dauerhaften Sperrung führen.
|
||||
|
||||
### 4. Dauerhafte Sperrung
|
||||
|
||||
**Auswirkungen auf die Community**: Wiederholte Verstöße gegen die Community-Standards,
|
||||
einschließlich anhaltend unangemessenem Verhalten, Belästigung einer
|
||||
Person oder Aggression gegenüber oder Herabwürdigung von Personengruppen.
|
||||
|
||||
**Konsequenz**: Ein dauerhafter Ausschluss von jeglicher öffentlicher
|
||||
Interaktion innerhalb der Community.
|
||||
|
||||
## Quellenangabe
|
||||
|
||||
Dieser Verhaltenskodex ist eine Adaption des [Contributor Covenant][homepage],
|
||||
Version 2.0, verfügbar unter
|
||||
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
Die Richtlinien zu den Auswirkungen auf die Gemeinschaft wurden inspiriert von
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
Für Antworten auf häufig gestellte Fragen zu diesem Verhaltenskodex siehe die
|
||||
häufig gestellten Fragen (FAQ) unter
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Übersetzungen sind verfügbar
|
||||
unter [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
133
docs/CODE_OF_CONDUCT-KR.md
Normal file
133
docs/CODE_OF_CONDUCT-KR.md
Normal file
@@ -0,0 +1,133 @@
|
||||
|
||||
# 기여자 계약 행동 강령
|
||||
|
||||
## 우리의 서약
|
||||
|
||||
회원, 기여자, 리더로서 우리는 나이, 신체 크기, 눈에
|
||||
보이거나 보이지 않는 장애, 민족, 성 특성, 성 정체성 및
|
||||
표현, 경험 수준, 교육, 사회 경제적 지위, 국적, 외모,
|
||||
인종, 종교, 성적 정체성 및 지향에 관계없이 모든 사람이
|
||||
괴롭힘 없이 커뮤니티에 참여할 수 있도록 할 것을
|
||||
서약합니다.
|
||||
|
||||
우리는 개방적이고 환영하며 다양하고 포용적이며 건강한 커뮤니티에
|
||||
기여하는 방식으로 행동하고 교류할 것을 약속합니다.
|
||||
|
||||
## 우리의 표준
|
||||
|
||||
커뮤니티의 긍정적인 환경에 기여하는 행동의 예는
|
||||
다음과 같습니다:
|
||||
|
||||
* 다른 사람들에게 공감과 친절을 보여주기
|
||||
* 다양한 의견, 관점, 경험을 존중하기
|
||||
* 건설적인 피드백을 제공하고 우아하게 받아들이기
|
||||
* 우리의 실수로 인해 영향을 받은 사람들에게 책임을 받아들이고 사과하며
|
||||
그 경험을 통해 배우기
|
||||
* 우리 개인뿐만 아니라 전체 커뮤니티에 가장 좋은 것이 무엇인지
|
||||
집중하기
|
||||
|
||||
용납할 수 없는 행동의 예는 다음과 같습니다:
|
||||
|
||||
* 성적인 언어 또는 이미지의 사용, 모든 종류의 성적 관심 또는
|
||||
접근 행위
|
||||
* 트롤링, 모욕적이거나 경멸적인 댓글, 개인적 또는 정치적 공격
|
||||
* 공개적 또는 사적인 괴롭힘
|
||||
* 명시적인 허가 없이 타인의 실제 주소 또는 이메일 주소와 같은
|
||||
개인정보를 게시하는 행위
|
||||
* 직업적 환경에서 합리적으로 부적절하다고 간주될 수 있는
|
||||
기타 행위
|
||||
|
||||
## 시행 책임
|
||||
|
||||
커뮤니티 리더는 허용되는 행동의 기준을 명확히 하고 시행할
|
||||
책임이 있으며 부적절하거나 위협적이거나 모욕적이거나
|
||||
유해하다고 판단되는 행동에 대해 적절하고 공정한 시정 조치를
|
||||
취합니다.
|
||||
|
||||
커뮤니티 리더는 본 행동 강령에 부합하지 않는 댓글, 커밋,
|
||||
코드, 위키 편집, 이슈 및 기타 기여를 삭제, 편집 또는 거부할
|
||||
권한과 책임이 있으며, 적절한 경우 중재 결정의 이유를
|
||||
전달합니다.
|
||||
|
||||
## 범위
|
||||
|
||||
본 행동 강령은 모든 커뮤니티 공간에서 적용되며, 개인이 공개
|
||||
공간에서 커뮤니티를 공식적으로 대표하는 경우에도 적용됩니다.
|
||||
커뮤니티를 대표하는 예로는 공식 이메일 주소 사용, 공식 소셜 미디어
|
||||
계정을 통한 게시, 온라인 또는 오프라인 이벤트에서 지정된 대표자로
|
||||
활동하는 것 등이 있습니다.
|
||||
|
||||
## 시행
|
||||
|
||||
모욕적, 괴롭힘 또는 기타 용납할 수 없는 행동은
|
||||
[info@rustdesk.com](mailto:info@rustdesk.com)으로 법 집행을 담당하는 커뮤니티 리더에게
|
||||
신고하실 수 있습니다.
|
||||
모든 불만 사항은 신속하고 공정하게 검토 및 조사됩니다.
|
||||
|
||||
모든 커뮤니티 리더는 모든 사건 신고자의 사생활과 보안을 존중할 의무가
|
||||
있습니다.
|
||||
|
||||
## 시행 지침
|
||||
|
||||
커뮤니티 리더는 이 행동 강령을 위반한 것으로 간주되는 모든 행동에 대한
|
||||
결과를 결정할 때 다음 커뮤니티 영향 지침을 따릅니다:
|
||||
|
||||
### 1. 수정
|
||||
|
||||
**커뮤니티 영향**: 커뮤니티에서 비전문적이거나 환영받지 못하는
|
||||
것으로 간주되는 부적절한 언어 사용이나 기타 행위입니다.
|
||||
|
||||
**결과**: 커뮤니티 리더의 비공개 서면 경고. 위반 사항의 성격과
|
||||
해당 행동이 부적절했던 이유를 명확히 설명해야 합니다.
|
||||
공개 사과를 요청할 수도 있습니다.
|
||||
|
||||
### 2. 경고
|
||||
|
||||
**커뮤니티 영향**: 단일 사건 또는 일련의 행위를 통한
|
||||
위반입니다.
|
||||
|
||||
**결과**: 지속적인 행동에 대한 경고 및 결과. 행동 강령 시행 담당자와의
|
||||
원치 않는 상호작용을 포함하여 관련자와의 상호작용은 일정
|
||||
기간 동안 금지됩니다. 여기에는 공동 공간 및 소셜 미디어와
|
||||
같은 외부 채널에서의 상호작용 금지가 포함됩니다. 이러한
|
||||
조건을 위반할 경우 일시적 또는 영구적으로 이용이 금지될 수
|
||||
있습니다.
|
||||
|
||||
### 3. 일시 금지
|
||||
|
||||
**커뮤니티 영향**: 지속적인 부적절한 행동을 포함하여
|
||||
커뮤니티 기준을 심각하게 위반한 경우입니다.
|
||||
|
||||
**결과**: 일정 기간 동안 커뮤니티와의 모든 상호작용이나 공개적인 소통이
|
||||
일시적으로 금지됩니다. 이 기간 동안에는 행동 강령을 시행하는
|
||||
사람들과의 원치 않는 상호작용을 포함하여 관련자들과의 공개적 또는
|
||||
사적인 상호작용이 허용되지 않습니다.
|
||||
이러한 조건을 위반할 경우 영구적으로 이용이 금지될 수 있습니다.
|
||||
|
||||
### 4. 영구 금지
|
||||
|
||||
**커뮤니티 영향**: 지속적인 부적절한 행동, 특정 개인에 대한 괴롭힘,
|
||||
특정 계층에 대한 공격성 또는 비하 등 공동체 기준을 위반하는
|
||||
행동을 보이는 경우입니다.
|
||||
|
||||
**결과**: 공동체 내 모든 종류의 공개적인 상호작용이 영구적으로
|
||||
금지됩니다.
|
||||
|
||||
## 귀속
|
||||
|
||||
본 행동 강령은 [Contributor Covenant][homepage] 버전 2.0을 바탕으로 작성되었으며
|
||||
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]에서
|
||||
확인하실 수 있습니다.
|
||||
|
||||
커뮤니티 영향 지침은
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC]에서 영감을 받았습니다.
|
||||
|
||||
본 행동 강령에 대한 일반적인 질문은 [https://www.contributor-covenant.org/faq][FAQ]에서 FAQ를
|
||||
참조하세요. 번역은 [https://www.contributor-covenant.org/translations][translations]에서
|
||||
확인하실 수 있습니다.
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
85
docs/CODE_OF_CONDUCT-RO.md
Normal file
85
docs/CODE_OF_CONDUCT-RO.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Codul de Conduită al Contributorilor
|
||||
|
||||
## Angajamentul Nostru
|
||||
|
||||
Noi, ca membri, contribuitori și lideri, ne angajăm să facem ca participarea în comunitatea noastră să fie o experiență fără hărțuire pentru toată lumea, indiferent de vârstă, dimensiunea corpului, dizabilități vizibile sau invizibile, etnie, caracteristici sexuale, identitate și exprimare de gen, nivel de experiență, educație, statut socio-economic, naționalitate, aspect personal, rasă, religie sau identitate și orientare sexuală.
|
||||
|
||||
Ne angajăm să acționăm și să interacționăm în moduri care contribuie la o comunitate deschisă, primitoare, diversă, incluzivă și sănătoasă.
|
||||
|
||||
## Standardele Noastre
|
||||
|
||||
Exemple de comportamente care contribuie la un mediu pozitiv pentru comunitatea noastră includ:
|
||||
|
||||
* Demonstrarea empatiei și a bunătății față de ceilalți
|
||||
* Respectarea opiniilor, punctelor de vedere și experiențelor diferite
|
||||
* Oferirea și acceptarea cu grație a feedback-ului constructiv
|
||||
* Asumarea responsabilității și cererea de scuze celor afectați de greșelile noastre și învățarea din experiență
|
||||
* Concentrarea pe ceea ce este cel mai bun nu doar pentru noi ca indivizi, ci pentru întreaga comunitate
|
||||
|
||||
Exemple de comportamente inacceptabile includ:
|
||||
|
||||
* Utilizarea limbajului sau imaginilor sexualizate, precum și atenția sau avansurile sexuale de orice fel
|
||||
* Trollare, insulte sau comentarii denigratoare și atacuri personale sau politice
|
||||
* Hărțuire publică sau privată
|
||||
* Publicarea informațiilor private ale altora, cum ar fi adresa fizică sau de e-mail, fără permisiunea explicită
|
||||
* Alte comportamente care ar putea fi considerate inadecvate într-un cadru profesional
|
||||
|
||||
## Responsabilități de Aplicare
|
||||
|
||||
Liderii comunității sunt responsabili pentru clarificarea și aplicarea standardelor noastre de comportament acceptabil și vor lua măsuri corective adecvate și echitabile ca răspuns la orice comportament pe care îl consideră inadecvat, amenințător, ofensator sau dăunător.
|
||||
|
||||
Liderii comunității au dreptul și responsabilitatea de a elimina, edita sau respinge comentarii, commit-uri, cod, editări wiki, probleme și alte contribuții care nu se aliniază acestui Cod de Conduită și vor comunica motivele pentru deciziile de moderare atunci când este cazul.
|
||||
|
||||
## Domeniu de Aplicare
|
||||
|
||||
Acest Cod de Conduită se aplică în toate spațiile comunității și se aplică și atunci când un individ reprezintă oficial comunitatea în spații publice.
|
||||
Exemple de reprezentare a comunității includ utilizarea unei adrese de e-mail oficiale, postarea printr-un cont oficial de social media sau acționarea ca reprezentant desemnat la un eveniment online sau offline.
|
||||
|
||||
## Aplicare
|
||||
|
||||
Cazurile de comportament abuziv, hărțuitor sau altfel inacceptabil pot fi raportate liderilor comunității responsabili pentru aplicare la [info@rustdesk.com](mailto:info@rustdesk.com).
|
||||
Toate plângerile vor fi revizuite și investigate prompt și corect.
|
||||
|
||||
Toți liderii comunității sunt obligați să respecte confidențialitatea și securitatea persoanei care raportează orice incident.
|
||||
|
||||
## Ghiduri de Aplicare
|
||||
|
||||
Liderii comunității vor urma aceste Ghiduri privind Impactul Comunității pentru a stabili consecințele pentru orice acțiune pe care o consideră o încălcare a acestui Cod de Conduită:
|
||||
|
||||
### 1. Corectare
|
||||
|
||||
**Impact asupra comunității**: Utilizarea limbajului neadecvat sau alte comportamente considerate neprofesionale sau nedorite în comunitate.
|
||||
|
||||
**Consecință**: O avertizare scrisă și privată din partea liderilor comunității, oferind claritate asupra naturii încălcării și o explicație despre motivul pentru care comportamentul a fost inadecvat. Poate fi cerută o scuză publică.
|
||||
|
||||
### 2. Avertisment
|
||||
|
||||
**Impact asupra comunității**: Încălcare printr-un incident singular sau o serie de acțiuni.
|
||||
|
||||
**Consecință**: Un avertisment cu consecințe pentru continuarea comportamentului. Nicio interacțiune cu persoanele implicate, inclusiv interacțiuni nesolicitate cu cei care aplică Codul de Conduită, pentru o perioadă specificată. Aceasta include evitarea interacțiunilor în spațiile comunității, precum și pe canale externe, cum ar fi rețelele sociale. Încălcarea acestor termeni poate duce la o suspendare temporară sau permanentă.
|
||||
|
||||
### 3. Suspendare Temporară
|
||||
|
||||
**Impact asupra comunității**: O încălcare serioasă a standardelor comunității, inclusiv comportament neadecvat susținut.
|
||||
|
||||
**Consecință**: Suspendare temporară de la orice tip de interacțiune sau comunicare publică cu comunitatea pentru o perioadă specificată. Nicio interacțiune publică sau privată cu persoanele implicate, inclusiv interacțiuni nesolicitate cu cei care aplică Codul de Conduită, nu este permisă în această perioadă. Încălcarea acestor termeni poate duce la o interdicție permanentă.
|
||||
|
||||
### 4. Interdicție Permanentă
|
||||
|
||||
**Impact asupra comunității**: Demonstrând un tipar de încălcare a standardelor comunității, inclusiv comportament neadecvat susținut, hărțuire a unei persoane sau agresiune față de sau denigrare a unor grupuri de persoane.
|
||||
|
||||
**Consecință**: Interdicție permanentă de la orice tip de interacțiune publică în cadrul comunității.
|
||||
|
||||
## Atribuire
|
||||
|
||||
Acest Cod de Conduită este adaptat din [Contributor Covenant][homepage], versiunea 2.0, disponibil la [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
Ghidurile privind Impactul Comunității au fost inspirate de [scara de aplicare a codului de conduită Mozilla][Mozilla CoC].
|
||||
|
||||
Pentru răspunsuri la întrebări frecvente despre acest cod de conduită, vezi FAQ la [https://www.contributor-covenant.org/faq][FAQ]. Traduceri sunt disponibile la [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -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)을 참조하세요.
|
||||
|
||||
## 행동 강령
|
||||
|
||||
|
||||
31
docs/CONTRIBUTING-RO.md
Normal file
31
docs/CONTRIBUTING-RO.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Contribuții la RustDesk
|
||||
|
||||
RustDesk primește cu plăcere contribuții din partea tuturor. Iată ghidurile dacă te gândești să ne ajuți:
|
||||
|
||||
## Contribuții
|
||||
|
||||
Contribuțiile la RustDesk sau la dependențele sale ar trebui făcute sub forma de pull request-uri pe GitHub. Fiecare pull request va fi revizuit de un contributor principal (cineva cu permisiunea de a aplica patch-uri) și fie va fi integrat în arborele principal, fie vor fi oferite sugestii pentru modificările necesare. Toate contribuțiile trebuie să urmeze acest format, chiar și cele ale contributorilor principali.
|
||||
|
||||
Dacă dorești să lucrezi la o problemă, te rugăm să o revendici mai întâi comentând pe GitHub issue-ul pe care vrei să lucrezi. Aceasta previne eforturi duplicate din partea contributorilor asupra aceleiași probleme.
|
||||
|
||||
## Lista de verificare pentru Pull Request
|
||||
|
||||
- Creează un branch din branch-ul `master` și, dacă este necesar, fă rebase la branch-ul `master` curent înainte de a trimite pull request-ul. Dacă nu se poate integra curat cu `master`, ți se poate cere să faci rebase la modificările tale.
|
||||
|
||||
- Commit-urile ar trebui să fie cât mai mici posibil, asigurând totodată că fiecare commit este corect independent (adică fiecare commit ar trebui să compileze și să treacă testele).
|
||||
|
||||
- Commit-urile trebuie să fie însoțite de un semnătura Developer Certificate of Origin (http://developercertificate.org), care indică faptul că tu (și angajatorul tău, dacă este cazul) ești de acord să respecți termenii [licenței proiectului](../LICENCE). În git, aceasta este opțiunea `-s` la `git commit`.
|
||||
|
||||
- Dacă patch-ul tău nu este revizuit sau ai nevoie ca o anumită persoană să-l revizuiască, poți @-reply unui reviewer cerând o revizuire în pull request sau într-un comentariu, sau poți solicita o revizuire prin [email](mailto:info@rustdesk.com).
|
||||
|
||||
- Adaugă teste relevante pentru bug-ul corectat sau pentru funcționalitatea nouă.
|
||||
|
||||
Pentru instrucțiuni specifice git, vezi [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||
|
||||
## Conduită
|
||||
|
||||
[Codul de Conduită RustDesk](https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md)
|
||||
|
||||
## Comunicare
|
||||
|
||||
Contributorii RustDesk frecventează [Discord](https://discord.gg/nDceKgxnkV).
|
||||
@@ -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,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
.Rustبرنامج آخر لسطح المكتب عن بعد، مكتوب بـ
|
||||
يعمل خارج الصندوق، لا حاجة إلى إعدادات. لديك سيطرة كاملة على بياناتك، دون مخاوف بشأن الأمن. يمكنك استخدام خادم
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
|
||||
## التبعيات
|
||||
|
||||
لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Zase další software pro přístup k ploše na dálku, naprogramovaný v jazyce Rust. Funguje hned tak, jak je – není třeba žádného nastavování. Svá data máte ve svých rukách, bez obav o zabezpečení. Je možné používat námi poskytovaný propojovací/předávací (relay) server, [vytvořit si svůj vlastní](https://rustdesk.com/server), nebo [si dokonce svůj vlastní naprogramovat](https://github.com/rustdesk/rustdesk-server-demo), budete-li chtít.
|
||||
|
||||
|
||||
@@ -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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
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,9 +14,9 @@
|
||||
> 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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Sie haben die volle Kontrolle über Ihre Daten und müssen sich keine Sorgen um die Sicherheit machen. Sie können unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Denove alia fora labortabla programo, skribita en Rust. Ĝi funkcias elskatole, ne bezonas konfiguraĵon. Vi havas la tutan kontrolon sur viaj datumoj, sen zorgo pri sekureco. Vi povas uzi nian servilon rendezvous/relajsan, [agordi vian propran](https://rustdesk.com/server), aŭ [skribi vian propran servilon rendezvous/relajsan](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
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://rustdesk.com/pricing.html)
|
||||
|
||||
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,10 +9,10 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
راستدسک (RustDesk) نرمافزاری برای کارکردن با رایانهی رومیزی از راه دور است و با زبان برنامهنویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آنها کنترل کامل داشته باشید.
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Vielä yksi etätyöpöytäohjelmisto, ohjelmoitu Rust-kielellä. Toimii suoraan pakkauksesta, ei tarvitse asetusta. Hallitset täysin tietojasi, ei tarvitse murehtia turvallisuutta. Voit käyttää meidän rendezvous/relay-palvelinta, [aseta omasi](https://rustdesk.com/server), tai [kirjoittaa oma rendezvous/relay-palvelin](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Encore un autre logiciel de bureau à distance, écrit en Rust. Fonctionne directement, aucune configuration n'est nécessaire. Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, [configurer le vôtre](https://rustdesk.com/server), ou [écrire votre propre serveur de rendez-vous/relais](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Ένα λογισμικό απομακρυσμένης επιφάνειας εργασίας, γραμμένο σε γλώσσα Rust. Δεν χρειάζεται κάποια παραμετροποίηση, λειτουργεί αμέσως μετά την εγκατάσταση. Έχετε τον πλήρη έλεγχο των δεδομένων σας, χωρίς να ανησυχείτε για την ασφάλειά τους. Μπορείτε να χρησιμοποιήσετε τους προκαθορισμένους διακομιστές rendezvous/αναμετάδοσης, [να εγκαταστήσετε τον δικό σας διακομιστή](https://rustdesk.com/server), ή [να αναπτύξετε ένα δικό σας διακομιστή rendezvous/αναμετάδοσης](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε.
|
||||
Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε.
|
||||
|
||||
[**Συχνές ερωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
A RustDesk egy távoli elérésű asztali szoftver, Rust-ban írva. Működik mindenféle konfiguráció nélkül, feltelepítéssel, vagy anélkül. Az adataidat teljesen te kezeled, nincs szükség aggódásra a harmadik felek miatt. Használhatod a RustDesk punblikus randevú/relay szervereit, [hostolhatsz sajátot](https://rustdesk.com/server), vagy akár [írhatsz is egyet](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを作成する](https://github.com/rustdesk/rustdesk-server-demo)こともできます。
|
||||
|
||||

|
||||
|
||||
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="#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="#빌드를 위한 원시 단계">빌드</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-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>] | [<a href="README-NO.md">Norsk</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의 개발자는 이 소프트웨어의 비윤리적 또는 불법적인 사용을 묵인하거나 지원하지 않습니다. 무단 액세스, 제어 또는 개인정보 침해와 같은 오용은 엄격하게 당사의 지침에 위배됩니다. 작성자는 응용 프로그램의 오용에 대해 책임을 지지 않습니다.
|
||||
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
우리와 채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [직접 설정](https://rustdesk.com/server)하거나 [직접 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
또 하나의 원격 데스크톱 솔루션으로, Rust로 작성되었습니다. 별도의 설정 없이 바로 사용할 수 있습니다. 데이터에 대한 완전한 통제권을 가지며 보안에 대한 걱정이 없습니다. 저희 랑데부/릴레이 서버를 사용하거나, [직접 설정](https://rustdesk.com/server)하거나, [자신만의 랑데부/릴레이 서버를 작성](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,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
റസ്റ്റിൽ എഴുതിയ മറ്റൊരു റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്വെയർ. ബോക്സിന് പുറത്ത് പ്രവർത്തിക്കുന്നു, കോൺഫിഗറേഷൻ ആവശ്യമില്ല. സുരക്ഷയെക്കുറിച്ച് ആശങ്കകളൊന്നുമില്ലാതെ, നിങ്ങളുടെ ഡാറ്റയുടെ പൂർണ്ണ നിയന്ത്രണം നിങ്ങൾക്കുണ്ട്. നിങ്ങൾക്ക് ഞങ്ങളുടെ rendezvous/relay സെർവർ ഉപയോഗിക്കാം, [സ്വന്തമായി സജ്ജീകരിക്കുക](https://rustdesk.com/server), അല്ലെങ്കിൽ [നിങ്ങളുടെ സ്വന്തം rendezvous/relay സെർവർ എഴുതുക](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Alweer een andere programma voor -bureaublad op afstand-, geschreven in Rust. Werkt -out of the box-, geen configuratie nodig. U heeft volledige controle over uw gegevens, en hoeft zich geen zorgen te maken over de beveiliging. U kunt onze rendez-vous/relay server gebruiken, [je eigen server opzetten](https://rustdesk.com/blog/id-relay-set), of [je eigen rendez-vous/relay-server schrijven](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av pakken, ingen konfigurasjon nødvendig. Du har full kontroll over din data, uten beskymring for sikkerhet. Du kan bruke vår rendezvous_mediator/relay server, [sett opp din egen](https://rustdesk.com/server), eller [skriv din egen rendezvous_mediator/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
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,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego, [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Mais um software de desktop remoto, escrito em Rust. Funciona por padrão, sem necessidade de configuração. Você tem completo controle de seus dados, sem se preocupar com segurança. Você pode usar nossos servidores de rendezvous/relay, [configurar seu próprio](https://rustdesk.com/server), ou [escrever seu próprio servidor de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
181
docs/README-RO.md
Normal file
181
docs/README-RO.md
Normal file
@@ -0,0 +1,181 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - desktopul tău la distanță"><br>
|
||||
<a href="../README.md#raw-steps-to-build">Construire</a> •
|
||||
<a href="../README.md#how-to-build-with-docker">Docker</a> •
|
||||
<a href="../README.md#file-structure">Structură</a> •
|
||||
<a href="../README.md#snapshot">Capturi</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>] | [<a href="README-NO.md">Norsk</a>] | [<a href="README-RO.md">Română</a>]<br>
|
||||
<b>Avem nevoie de ajutorul tău pentru a traduce acest README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> și <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> în limba ta maternă</b>
|
||||
</p>
|
||||
|
||||
> [!Atenție]
|
||||
> **Declinare de responsabilitate privind utilizarea abuzivă:** <br>
|
||||
> Dezvoltatorii RustDesk nu susțin sau aprobă utilizarea neetică sau ilegală a acestui software. Utilizarea abuzivă, cum ar fi accesul neautorizat, controlul sau invadarea intimității, este strict împotriva regulilor noastre. Autorii nu sunt responsabili pentru utilizarea necorespunzătoare a aplicației.
|
||||
|
||||
|
||||
Conversați cu noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Încă o soluție de desktop la distanță scrisă în Rust. Funcționează imediat, fără configurare necesară. Ai control total asupra datelor tale, fără probleme de securitate. Poți folosi serverul nostru de rendezvous/relay, [să-ți configurezi propriul server](https://rustdesk.com/server) sau [să scrii propriul server de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk primește contribuții de la oricine. Vezi [CONTRIBUTING.md](../docs/CONTRIBUTING.md) pentru ajutor la început.
|
||||
|
||||
[**ÎNTREBĂRI FRECVENTE (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**DESCĂRCARE BINARE**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**BUILD NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<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)
|
||||
|
||||
## Dependențe
|
||||
|
||||
Versiunile desktop folosesc Flutter sau Sciter (depreciat) pentru interfață; acest ghid este pentru Sciter doar, deoarece este mai ușor și mai prietenos pentru început. Vezi [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) pentru construire cu Flutter.
|
||||
|
||||
Te rugăm să descarci singur librăria dinamică 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)
|
||||
|
||||
## Pași pentru construire (Raw Steps to build)
|
||||
|
||||
- Pregătește mediul de dezvoltare Rust și mediul de construire C++
|
||||
|
||||
- Instalează [vcpkg](https://github.com/microsoft/vcpkg) și setează corect variabila de mediu `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
|
||||
|
||||
- rulează `cargo run`
|
||||
|
||||
## [Construire](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Cum se construiește pe Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev 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 gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Instalează vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2023.04.15
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### Repară libvpx (Pentru Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
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
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Cum să construiești cu Docker
|
||||
|
||||
Începe prin clonarea repository-ului și construirea imaginii Docker:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
git submodule update --init --recursive
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Apoi, de fiecare dată când trebuie să construiești aplicația, rulează comanda următoare:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Reține că prima construire poate dura mai mult până când dependențele sunt în cache; construirile ulterioare vor fi mai rapide. De asemenea, dacă trebuie să specifici argumente diferite comenzii de build, le poți adăuga la finalul comenzii în poziția `<OPTIONAL-ARGS>`. De exemplu, pentru a construi o versiune optimizată de release, adaugă `--release`. Executabilul rezultat va fi disponibil în folderul `target` pe sistemul tău, și poate fi rulat cu:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Sau, dacă rulezi un executabil release:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Asigură-te că rulezi aceste comenzi din rădăcina repository-ului RustDesk, altfel aplicația poate să nu găsească resursele necesare. De asemenea, reține că alte subcomenzi cargo, cum ar fi `install` sau `run`, nu sunt acceptate în prezent prin această metodă, deoarece ar instala sau rula programul în interiorul containerului în loc de gazdă.
|
||||
|
||||
## Structura fișierelor
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funcții fs pentru transfer de fișiere și alte funcții utilitare
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: capturare ecran
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control tastatură/mouse specific platformei
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementare copy/paste pentru fișiere pentru Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interfață Sciter învechită (depreciată)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servicii audio/clipboard/input/video și conexiuni de rețea
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: inițiază o conexiune peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript pentru clientul Flutter web
|
||||
|
||||
## Capturi de ecran
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -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 не одобряют и не поддерживают какое-либо неэтичное или незаконное использование данного программного обеспечения. Неправомерное использование (несанкционированный доступ, контроль или вторжение в частную жизнь) строго противоречит нашим правилам. Авторы не несут ответственности за любое неправомерное использование приложения.
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
Общение с нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||
|
||||
Еще одно программное обеспечение для удаленного рабочего стола, написанное на Rust. Работает из коробки, не требует настройки. Вы полностью контролируете свои данные, не беспокоясь о безопасности. Вы можете использовать наш сервер ретрансляции, [настроить свой собственный](https://rustdesk.com/server), или [написать свой](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Ещё одно программное обеспечение для удаленного рабочего стола, написанное на 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
|
||||
|
||||
## Скриншоты
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
@@ -7,34 +7,37 @@
|
||||
<a href="#file-structure">Dosya Yapısı</a> •
|
||||
<a href="#snapshot">Ekran Görüntüleri</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>]<br>
|
||||
<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>
|
||||
<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 Dökümantasyonu</a>'nu 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)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
> [!Dikkat]
|
||||
> **Yanlış Kullanım Uyarısı:** <br>
|
||||
> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
|
||||
|
||||
Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kullanıma hazır, hiçbir yapılandırma gerektirmez. Verilerinizin tam kontrolünü elinizde tutarsınız ve güvenlikle ilgili endişeleriniz olmaz. Kendi buluş/iletme sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi buluş/iletme sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
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://rustdesk.com/pricing.html)
|
||||
|
||||
Rust dilinde yazılmış, başka bir uzak masaüstü yazılımı daha. Hiçbir yapılandırma gerekmeksizin, hemen kullanıma hazır. Güvenlik konusunda hiçbir endişe duymadan, verileriniz üzerinde tam kontrole sahip olun. Kendi rendezvous/relay sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi rendezvous/relay sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın.
|
||||
RustDesk, herkesin katkısına açıktır. Başlamak için [CONTRIBUTING.md](CONTRIBUTING-TR.md) belgesine göz atın.
|
||||
|
||||
[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**BİNARİ İNDİR**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**BINARY İNDİR**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**NİGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
[**NIGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="F-Droid'de Alın"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Bağımlılıklar
|
||||
## Gereksinimler
|
||||
|
||||
Masaüstü sürümleri GUI için
|
||||
|
||||
[Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
|
||||
Masaüstü sürümleri GUI için; [Sciter](https://sciter.com/)(kaldırılacak) veya Flutter kullanır. Sciter daha kolay ve başlamak için daha dostcanlısı, bundan dolayı bu kılavuz sadece Sciter içindir. Flutter sürümünü derlemek için [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)'ımıza bakın.
|
||||
|
||||
Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
|
||||
|
||||
@@ -46,7 +49,7 @@ Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
|
||||
|
||||
- Rust geliştirme ortamınızı ve C++ derleme ortamınızı hazırlayın.
|
||||
|
||||
- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` çevresel değişkenini doğru bir şekilde ayarlayın.
|
||||
- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` ortam değişkenini doğru bir şekilde ayarlayın.
|
||||
|
||||
- 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
|
||||
@@ -123,7 +126,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
|
||||
## Docker ile Derleme Nasıl Yapılır
|
||||
|
||||
Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun:
|
||||
Önce repository'i klonlayın ve Docker container'ını oluşturun.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
@@ -131,44 +134,40 @@ cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Ardından, uygulamayı derlemek için her seferinde aşağıdaki komutu çalıştırın:
|
||||
Ardından, uygulamayı her derlemeniz gerektiğinde aşağıdaki komutu çalıştırın:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
İlk derleme, bağımlılıklar önbelleğe alınmadan önce daha uzun sürebilir, sonraki derlemeler daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu
|
||||
|
||||
komutun sonunda `<İSTEĞE BAĞLI-ARGÜMANLAR>` pozisyonunda yapabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan yürütülebilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir:
|
||||
Bilin ki ilk derlemeniz gereksinimlerin önbelleği yüklenmesinden ötürü uzun sürebilir, sonraki derlemeleriniz daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu komutun sonunda ki `<OPTIONAL-ARGS>` yerine yazabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan çalıştırılabilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir olacaktır:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Veya, yayın yürütülebilir dosyası çalıştırılıyorsa:
|
||||
Veya, yayım çalıştırılabilir dosyası için:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır ve ana makinede değil.
|
||||
Lütfen bu komutları RustDesk reposunun root klasöründe çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır, ana makinede değil.
|
||||
|
||||
## Dosya Yapısı
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodlayıcı, yapılandırma, tcp/udp sarmalayıcı, protobuf, dosya transferi için fs işlevleri ve diğer bazı yardımcı işlevler
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, dosya transferi için fs fonksiyonları ve diğer bazı yardımcı işlevler
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ekran yakalama
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platforma özgü klavye/fare kontrolü
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pasta/klavye/video hizmetleri ve ağ bağlantıları
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bir eş bağlantısı başlatır
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişim kurar, uzak doğrudan (TCP delik vurma) veya iletme bağlantısını bekler
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: platforma özgü kopyala/yapıştır implementasyonları.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Eski Sciter UI (kaldırılacak)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pano/input/video servisleri ve ağ bağlantıları
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Eşli bağlantı başlat
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter web istemcisi için JavaScript
|
||||
|
||||
> [!Dikkat]
|
||||
> **Yanlış Kullanım Uyarısı:** <br>
|
||||
> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
|
||||
|
||||
## Ekran Görüntüleri
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](docs/CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
|
||||
RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
|
||||
|
||||
[**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
[<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)
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
远程桌面软件,开箱即用,无需任何配置。您完全掌控数据,不用担心安全问题。您可以使用我们的注册/中继服务器,
|
||||
或者[自己设置](https://rustdesk.com/server),
|
||||
|
||||
7
docs/SECURITY-KR.md
Normal file
7
docs/SECURITY-KR.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 보안 정책
|
||||
|
||||
## 취약점 보고
|
||||
|
||||
저희는 프로젝트의 보안을 매우 중요하게 생각합니다. 모든 사용자가 발견한 취약점을 저희에게 보고할 것을 권장합니다. RustDesk 프로젝트에서 보안 취약점이 발견되면 info@rustdesk.com으로 이메일을 보내 책임감 있게 보고해 주시기 바랍니다.
|
||||
|
||||
현재로서는 버그 현상금 프로그램이 없습니다. 저희는 큰 문제를 해결하기 위해 노력하는 소규모 팀입니다. 전체 커뮤니티를 위한 안전한 응용 프로그램을 계속 구축할 수 있도록 취약점을 책임감 있게 신고해 주시기 바랍니다.
|
||||
9
docs/SECURITY-RO.md
Normal file
9
docs/SECURITY-RO.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Politica de Securitate
|
||||
|
||||
## Raportarea unei Vulnerabilități
|
||||
|
||||
Acordăm o mare importanță securității proiectului. Încurajăm toți utilizatorii să ne raporteze orice vulnerabilități pe care le descoperă.
|
||||
Dacă găsești o vulnerabilitate de securitate în proiectul RustDesk, te rugăm să o raportezi responsabil trimițând un e-mail la info@rustdesk.com.
|
||||
|
||||
În acest moment, nu avem un program de recompense pentru descoperirea de bug-uri. Suntem o echipă mică care încearcă să rezolve o problemă mare.
|
||||
Te rugăm să raportezi orice vulnerabilitate în mod responsabil, astfel încât să putem continua să construim o aplicație sigură pentru întreaga comunitate.
|
||||
@@ -1 +1 @@
|
||||
An open-source remote desktop application, the open source TeamViewer alternative.
|
||||
An open-source remote desktop application, the TeamViewer alternative
|
||||
|
||||
@@ -55,8 +55,7 @@
|
||||
],
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=fallback-x11",
|
||||
"--socket=wayland",
|
||||
"--socket=x11",
|
||||
"--share=network",
|
||||
"--filesystem=home",
|
||||
"--device=dri",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import com.google.protobuf.gradle.*
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
plugins {
|
||||
id "com.google.protobuf" version "0.9.4"
|
||||
id "com.android.application"
|
||||
@@ -30,8 +32,37 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||
// Add rustls-platform-verifier Android support
|
||||
String findRustlsPlatformVerifierMavenDir() {
|
||||
def dependencyText = providers.exec {
|
||||
it.workingDir = new File("../..")
|
||||
commandLine("cargo", "metadata", "--format-version", "1")
|
||||
}.standardOutput.asText.get()
|
||||
|
||||
def dependencyJson = new JsonSlurper().parseText(dependencyText)
|
||||
def pkg = dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }
|
||||
|
||||
if (pkg == null) {
|
||||
throw new GradleException("rustls-platform-verifier-android package not found in cargo metadata!")
|
||||
}
|
||||
|
||||
def manifestPath = file(pkg.manifest_path)
|
||||
def mavenDir = new File(manifestPath.parentFile, "maven")
|
||||
|
||||
if (!mavenDir.exists()) {
|
||||
throw new GradleException("Maven directory not found at: ${mavenDir.path}")
|
||||
}
|
||||
|
||||
println("✓ Found rustls-platform-verifier maven repo at: ${mavenDir.path}")
|
||||
return mavenDir.path
|
||||
}
|
||||
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url = findRustlsPlatformVerifierMavenDir()
|
||||
metadataSources.artifact()
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
@@ -67,7 +98,7 @@ android {
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.carriez.flutter_hbb"
|
||||
minSdkVersion 21
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
@@ -97,8 +128,10 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||
implementation "androidx.media:media:1.6.0"
|
||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation "rustls:rustls-platform-verifier:0.1.1"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Keep class members from protobuf generated code.
|
||||
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
|
||||
# Keep rustls-platform-verifier classes for JNI
|
||||
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
|
||||
@@ -23,6 +23,7 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="RustDesk"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -62,7 +62,13 @@ class MainActivity : FlutterActivity() {
|
||||
channelTag
|
||||
)
|
||||
initFlutterChannel(flutterMethodChannel!!)
|
||||
thread { setCodecInfo() }
|
||||
thread {
|
||||
try {
|
||||
setCodecInfo()
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Failed to setCodecInfo: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -316,7 +322,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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import ffi.FFI
|
||||
|
||||
class MainApplication : Application() {
|
||||
companion object {
|
||||
private const val TAG = "MainApplication"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "App start")
|
||||
FFI.onAppStart(applicationContext)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,6 +13,7 @@ object FFI {
|
||||
}
|
||||
|
||||
external fun init(ctx: Context)
|
||||
external fun onAppStart(ctx: Context)
|
||||
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
||||
external fun startServer(app_dir: String, custom_client_config: String)
|
||||
external fun startService()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -60,6 +62,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,11 +13,13 @@ import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -30,6 +32,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;
|
||||
@@ -41,6 +44,7 @@ import 'package:flutter_hbb/native/win32.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
|
||||
import 'package:flutter_hbb/native/common.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/common.dart';
|
||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||
|
||||
final globalKey = GlobalKey<NavigatorState>();
|
||||
final navigationBarKey = GlobalKey();
|
||||
@@ -73,6 +77,9 @@ bool _ignoreDevicePixelRatio = true;
|
||||
int windowsBuildNumber = 0;
|
||||
DesktopType? desktopType;
|
||||
|
||||
// Tolerance used for floating-point position comparisons to avoid precision errors.
|
||||
const double _kPositionEpsilon = 1e-6;
|
||||
|
||||
bool get isMainDesktopWindow =>
|
||||
desktopType == DesktopType.main || desktopType == DesktopType.cm;
|
||||
|
||||
@@ -99,10 +106,15 @@ enum DesktopType {
|
||||
remote,
|
||||
fileTransfer,
|
||||
viewCamera,
|
||||
terminal,
|
||||
cm,
|
||||
portForward,
|
||||
}
|
||||
|
||||
bool isDoubleEqual(double a, double b) {
|
||||
return (a - b).abs() < _kPositionEpsilon;
|
||||
}
|
||||
|
||||
class IconFont {
|
||||
static const _family1 = 'Tabbar';
|
||||
static const _family2 = 'PeerSearchbar';
|
||||
@@ -1152,15 +1164,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 +1196,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 +1228,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 +1583,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 +1593,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;
|
||||
@@ -1595,7 +1631,8 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
|
||||
// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
|
||||
// Because all session options use `Y` and `<Empty>` as values.
|
||||
|
||||
Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||
Future<bool> matchPeer(
|
||||
String searchText, Peer peer, PeerTabIndex peerTabIndex) async {
|
||||
if (searchText.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
@@ -1606,11 +1643,14 @@ Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||
peer.username.toLowerCase().contains(searchText)) {
|
||||
return true;
|
||||
}
|
||||
final alias = peer.alias;
|
||||
if (alias.isEmpty) {
|
||||
return false;
|
||||
if (peer.alias.toLowerCase().contains(searchText)) {
|
||||
return true;
|
||||
}
|
||||
return alias.toLowerCase().contains(searchText);
|
||||
if (peerTabShowNote(peerTabIndex) &&
|
||||
peer.note.toLowerCase().contains(searchText)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get the image for the current [platform].
|
||||
@@ -1640,6 +1680,15 @@ class LastWindowPosition {
|
||||
LastWindowPosition(this.width, this.height, this.offsetWidth,
|
||||
this.offsetHeight, this.isMaximized, this.isFullscreen);
|
||||
|
||||
bool equals(LastWindowPosition other) {
|
||||
return ((width == other.width) &&
|
||||
(height == other.height) &&
|
||||
(offsetWidth == other.offsetWidth) &&
|
||||
(offsetHeight == other.offsetHeight) &&
|
||||
(isMaximized == other.isMaximized) &&
|
||||
(isFullscreen == other.isFullscreen));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
"width": width,
|
||||
@@ -1679,24 +1728,36 @@ String get windowFramePrefix =>
|
||||
? "incoming_"
|
||||
: (bind.isOutgoingOnly() ? "outgoing_" : ""));
|
||||
|
||||
typedef WindowKey = ({WindowType type, int? windowId});
|
||||
|
||||
LastWindowPosition? _lastWindowPosition = null;
|
||||
final Debouncer _saveWindowDebounce = Debouncer(delay: Duration(seconds: 1));
|
||||
|
||||
/// Save window position and size on exit
|
||||
/// Note that windowId must be provided if it's subwindow
|
||||
Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
Future<void> saveWindowPosition(WindowType type,
|
||||
{int? windowId, bool? flush}) async {
|
||||
if (type != WindowType.Main && windowId == null) {
|
||||
debugPrint(
|
||||
"Error: windowId cannot be null when saving positions for sub window");
|
||||
}
|
||||
|
||||
late Offset position;
|
||||
late Size sz;
|
||||
Offset? position;
|
||||
Size? sz;
|
||||
late bool isMaximized;
|
||||
bool isFullscreen = stateGlobal.fullscreen.isTrue;
|
||||
|
||||
setPreFrame() {
|
||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
position = Offset(
|
||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
||||
if (lpos != null) {
|
||||
if (lpos.offsetWidth != null && lpos.offsetHeight != null) {
|
||||
position = Offset(lpos.offsetWidth!, lpos.offsetHeight!);
|
||||
}
|
||||
if (lpos.width != null && lpos.height != null) {
|
||||
sz = Size(lpos.width!, lpos.height!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
@@ -1736,30 +1797,56 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (isWindows) {
|
||||
if (isWindows && position != null) {
|
||||
const kMinOffset = -10000;
|
||||
const kMaxOffset = 10000;
|
||||
if (position.dx < kMinOffset ||
|
||||
position.dy < kMinOffset ||
|
||||
position.dx > kMaxOffset ||
|
||||
position.dy > kMaxOffset) {
|
||||
if (position!.dx < kMinOffset ||
|
||||
position!.dy < kMinOffset ||
|
||||
position!.dx > kMaxOffset ||
|
||||
position!.dy > kMaxOffset) {
|
||||
debugPrint("Invalid position: $position, ignore saving position");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final pos = LastWindowPosition(
|
||||
sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
|
||||
debugPrint(
|
||||
"Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
|
||||
final pos = LastWindowPosition(sz?.width, sz?.height, position?.dx,
|
||||
position?.dy, isMaximized, isFullscreen);
|
||||
|
||||
await bind.setLocalFlutterOption(
|
||||
k: windowFramePrefix + type.name, v: pos.toString());
|
||||
final WindowKey key = (type: type, windowId: windowId);
|
||||
|
||||
if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) &&
|
||||
windowId != null) {
|
||||
await _saveSessionWindowPosition(
|
||||
type, windowId, isMaximized, isFullscreen, pos);
|
||||
final bool haveNewWindowPosition =
|
||||
(_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!);
|
||||
final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning;
|
||||
|
||||
if (haveNewWindowPosition || isPreviousNewWindowPositionPending) {
|
||||
_lastWindowPosition = pos;
|
||||
|
||||
if (flush ?? false) {
|
||||
// If a previous update is pending, replace it.
|
||||
_saveWindowDebounce.cancel();
|
||||
await _saveWindowPositionActual(key);
|
||||
} else if (haveNewWindowPosition) {
|
||||
_saveWindowDebounce.call(() => _saveWindowPositionActual(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveWindowPositionActual(WindowKey key) async {
|
||||
LastWindowPosition? pos = _lastWindowPosition;
|
||||
|
||||
if (pos != null) {
|
||||
debugPrint(
|
||||
"Saving frame: ${key.windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
|
||||
|
||||
await bind.setLocalFlutterOption(
|
||||
k: windowFramePrefix + key.type.name, v: pos.toString());
|
||||
|
||||
if ((key.type == WindowType.RemoteDesktop ||
|
||||
key.type == WindowType.ViewCamera) &&
|
||||
key.windowId != null) {
|
||||
await _saveSessionWindowPosition(key.type, key.windowId!,
|
||||
pos.isMaximized ?? false, pos.isFullscreen ?? false, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1825,6 +1912,8 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
|
||||
return Size(restoreWidth, restoreHeight);
|
||||
}
|
||||
|
||||
// Consider using Rect.contains() instead,
|
||||
// though the implementation is not exactly the same.
|
||||
bool isPointInRect(Offset point, Rect rect) {
|
||||
return point.dx >= rect.left &&
|
||||
point.dx <= rect.right &&
|
||||
@@ -1922,8 +2011,24 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
if (lpos == null) {
|
||||
debugPrint("no window position saved, ignoring position restoration");
|
||||
return false;
|
||||
debugPrint("No window position saved, trying to center the window.");
|
||||
switch (type) {
|
||||
case WindowType.Main:
|
||||
// Center the main window only if no position is saved (on first run).
|
||||
if (isWindows || isLinux) {
|
||||
await windowManager.center();
|
||||
}
|
||||
// For MacOS, the window is already centered by default.
|
||||
// See https://github.com/rustdesk/rustdesk/blob/9b9276e7524523d7f667fefcd0694d981443df0e/flutter/macos/Runner/Base.lproj/MainMenu.xib#L333
|
||||
// If `<windowPositionMask>` in `<window>` is not set, the window will be centered.
|
||||
break;
|
||||
default:
|
||||
// No need to change the position of a sub window if no position is saved,
|
||||
// since the default position is already centered.
|
||||
// https://github.com/rustdesk/rustdesk/blob/317639169359936f7f9f85ef445ec9774218772d/flutter/lib/utils/multi_window_manager.dart#L163
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) {
|
||||
if (!isRemotePeerPos && windowId != null) {
|
||||
@@ -2097,6 +2202,11 @@ enum UriLinkType {
|
||||
viewCamera,
|
||||
portForward,
|
||||
rdp,
|
||||
terminal,
|
||||
}
|
||||
|
||||
setEnvTerminalAdmin() {
|
||||
bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y');
|
||||
}
|
||||
|
||||
// uri link handler
|
||||
@@ -2161,6 +2271,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 +2331,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 +2354,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 +2385,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 +2412,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 +2456,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 +2481,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 +2503,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 +2526,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 +2536,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 +2582,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 +2600,6 @@ connect(BuildContext context, String id,
|
||||
id: id,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
),
|
||||
),
|
||||
@@ -2462,10 +2609,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 +2638,6 @@ connect(BuildContext context, String id,
|
||||
id: id,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
),
|
||||
),
|
||||
@@ -2486,7 +2647,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),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2668,7 +2832,7 @@ class ServerConfig {
|
||||
} catch (err) {
|
||||
final input = msg.split('').reversed.join('');
|
||||
final bytes = base64Decode(base64.normalize(input));
|
||||
json = jsonDecode(utf8.decode(bytes));
|
||||
json = jsonDecode(utf8.decode(bytes, allowMalformed: true));
|
||||
}
|
||||
idServer = json['host'] ?? '';
|
||||
relayServer = json['relay'] ?? '';
|
||||
@@ -2785,7 +2949,7 @@ Future<void> updateSystemWindowTheme() async {
|
||||
///
|
||||
/// Note: not found a general solution for rust based AVFoundation bingding.
|
||||
/// [AVFoundation] crate has compile error.
|
||||
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
|
||||
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/host");
|
||||
|
||||
enum PermissionAuthorizeType {
|
||||
undetermined,
|
||||
@@ -2858,6 +3022,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 +3348,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 +3386,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 +3400,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 +3590,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(
|
||||
@@ -3815,3 +3989,39 @@ String get appName {
|
||||
}
|
||||
return _appName;
|
||||
}
|
||||
|
||||
String getConnectionText(bool secure, bool direct, String streamType) {
|
||||
String connectionText;
|
||||
if (secure && direct) {
|
||||
connectionText = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
connectionText = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
connectionText = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
connectionText = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
if (streamType == 'Relay') {
|
||||
streamType = 'TCP';
|
||||
}
|
||||
if (streamType.isEmpty) {
|
||||
return connectionText;
|
||||
} else {
|
||||
return '$connectionText ($streamType)';
|
||||
}
|
||||
}
|
||||
|
||||
String decode_http_response(http.Response resp) {
|
||||
try {
|
||||
// https://github.com/rustdesk/rustdesk-server-pro/discussions/758
|
||||
return utf8.decode(resp.bodyBytes, allowMalformed: true);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to decode response as UTF-8: $e');
|
||||
// Fallback to bodyString which handles encoding automatically
|
||||
return resp.body;
|
||||
}
|
||||
}
|
||||
|
||||
bool peerTabShowNote(PeerTabIndex peerTabIndex) {
|
||||
return peerTabIndex == PeerTabIndex.ab || peerTabIndex == PeerTabIndex.group;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -87,6 +89,7 @@ class PeerPayload {
|
||||
"platform": _platform(p.info['os']),
|
||||
"hostname": p.info['device_name'],
|
||||
"device_group_name": p.device_group_name,
|
||||
"note": p.note,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,15 +249,17 @@ class AbProfile {
|
||||
String name;
|
||||
String owner;
|
||||
String? note;
|
||||
dynamic info;
|
||||
int rule;
|
||||
|
||||
AbProfile(this.guid, this.name, this.owner, this.note, this.rule);
|
||||
AbProfile(this.guid, this.name, this.owner, this.note, this.rule, this.info);
|
||||
|
||||
AbProfile.fromJson(Map<String, dynamic> json)
|
||||
: guid = json['guid'] ?? '',
|
||||
name = json['name'] ?? '',
|
||||
owner = json['owner'] ?? '',
|
||||
note = json['note'] ?? '',
|
||||
info = json['info'],
|
||||
rule = json['rule'] ?? 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,9 +77,11 @@ class CurrentDisplayState {
|
||||
class ConnectionType {
|
||||
final Rx<String> _secure = kInvalidValueStr.obs;
|
||||
final Rx<String> _direct = kInvalidValueStr.obs;
|
||||
final Rx<String> _stream_type = kInvalidValueStr.obs;
|
||||
|
||||
Rx<String> get secure => _secure;
|
||||
Rx<String> get direct => _direct;
|
||||
Rx<String> get stream_type => _stream_type;
|
||||
|
||||
static String get strSecure => 'secure';
|
||||
static String get strInsecure => 'insecure';
|
||||
@@ -94,9 +96,14 @@ class ConnectionType {
|
||||
_direct.value = v ? strDirect : strIndirect;
|
||||
}
|
||||
|
||||
void setStreamType(String v) {
|
||||
_stream_type.value = v;
|
||||
}
|
||||
|
||||
bool isValid() {
|
||||
return _secure.value != kInvalidValueStr &&
|
||||
_direct.value != kInvalidValueStr;
|
||||
_direct.value != kInvalidValueStr &&
|
||||
_stream_type.value != kInvalidValueStr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -466,6 +466,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
IDTextEditingController idController = IDTextEditingController(text: '');
|
||||
TextEditingController aliasController = TextEditingController(text: '');
|
||||
TextEditingController passwordController = TextEditingController(text: '');
|
||||
TextEditingController noteController = TextEditingController(text: '');
|
||||
final tags = List.of(gFFI.abModel.currentAbTags);
|
||||
var selectedTag = List<dynamic>.empty(growable: true).obs;
|
||||
final style = TextStyle(fontSize: 14.0);
|
||||
@@ -494,7 +495,11 @@ class _AddressBookState extends State<AddressBook> {
|
||||
password = passwordController.text;
|
||||
}
|
||||
String? errMsg2 = await gFFI.abModel.addIdToCurrent(
|
||||
id, aliasController.text.trim(), password, selectedTag);
|
||||
id,
|
||||
aliasController.text.trim(),
|
||||
password,
|
||||
selectedTag,
|
||||
noteController.text);
|
||||
if (errMsg2 != null) {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
@@ -600,6 +605,24 @@ class _AddressBookState extends State<AddressBook> {
|
||||
),
|
||||
).workaroundFreezeLinuxMint(),
|
||||
)),
|
||||
row(
|
||||
label: Text(
|
||||
translate('Note'),
|
||||
style: style,
|
||||
),
|
||||
input: Obx(
|
||||
() => TextField(
|
||||
controller: noteController,
|
||||
maxLines: 3,
|
||||
minLines: 1,
|
||||
maxLength: 300,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('Note'),
|
||||
),
|
||||
).workaroundFreezeLinuxMint(),
|
||||
)),
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
|
||||
156
flutter/lib/common/widgets/custom_scale_base.dart
Normal file
156
flutter/lib/common/widgets/custom_scale_base.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/utils/scale.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
|
||||
/// Base class providing shared custom scale control logic for both mobile and desktop widgets.
|
||||
/// Implementations must provide [ffi] and [onScaleChanged] getters.
|
||||
abstract class CustomScaleControls<T extends StatefulWidget> extends State<T> {
|
||||
/// FFI instance for session interaction
|
||||
FFI get ffi;
|
||||
|
||||
/// Callback invoked when scale value changes
|
||||
ValueChanged<int>? get onScaleChanged;
|
||||
|
||||
late int _scaleValue;
|
||||
late final Debouncer<int> _debouncerScale;
|
||||
// Normalized slider position in [0, 1]. We map it nonlinearly to percent.
|
||||
double _scalePos = 0.0;
|
||||
|
||||
int get scaleValue => _scaleValue;
|
||||
double get scalePos => _scalePos;
|
||||
|
||||
int mapPosToPercent(double p) => _mapPosToPercent(p);
|
||||
|
||||
static const int minPercent = kScaleCustomMinPercent;
|
||||
static const int pivotPercent = kScaleCustomPivotPercent; // 100% should be at 1/3 of track
|
||||
static const int maxPercent = kScaleCustomMaxPercent;
|
||||
static const double pivotPos = kScaleCustomPivotPos; // first 1/3 → up to 100%
|
||||
static const double detentEpsilon = kScaleCustomDetentEpsilon; // snap range around pivot (~0.6%)
|
||||
|
||||
// Clamp helper for local use
|
||||
int _clampScale(int v) => clampCustomScalePercent(v);
|
||||
|
||||
// Map normalized position [0,1] → percent [5,1000] with 100 at 1/3 width.
|
||||
int _mapPosToPercent(double p) {
|
||||
if (p <= 0.0) return minPercent;
|
||||
if (p >= 1.0) return maxPercent;
|
||||
if (p <= pivotPos) {
|
||||
final q = p / pivotPos; // 0..1
|
||||
final v = minPercent + q * (pivotPercent - minPercent);
|
||||
return _clampScale(v.round());
|
||||
} else {
|
||||
final q = (p - pivotPos) / (1.0 - pivotPos); // 0..1
|
||||
final v = pivotPercent + q * (maxPercent - pivotPercent);
|
||||
return _clampScale(v.round());
|
||||
}
|
||||
}
|
||||
|
||||
// Map percent [5,1000] → normalized position [0,1]
|
||||
double _mapPercentToPos(int percent) {
|
||||
final p = _clampScale(percent);
|
||||
if (p <= pivotPercent) {
|
||||
final q = (p - minPercent) / (pivotPercent - minPercent);
|
||||
return q * pivotPos;
|
||||
} else {
|
||||
final q = (p - pivotPercent) / (maxPercent - pivotPercent);
|
||||
return pivotPos + q * (1.0 - pivotPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Snap normalized position to the pivot when close to it
|
||||
double _snapNormalizedPos(double p) {
|
||||
if ((p - pivotPos).abs() <= detentEpsilon) return pivotPos;
|
||||
if (p < 0.0) return 0.0;
|
||||
if (p > 1.0) return 1.0;
|
||||
return p;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scaleValue = 100;
|
||||
_debouncerScale = Debouncer<int>(
|
||||
kDebounceCustomScaleDuration,
|
||||
onChanged: (v) async {
|
||||
await _applyScale(v);
|
||||
},
|
||||
initialValue: _scaleValue,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
final v = await getSessionCustomScalePercent(ffi.sessionId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_scaleValue = v;
|
||||
_scalePos = _mapPercentToPos(v);
|
||||
});
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('[CustomScale] Failed to get initial value: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _applyScale(int v) async {
|
||||
v = clampCustomScalePercent(v);
|
||||
setState(() {
|
||||
_scaleValue = v;
|
||||
});
|
||||
try {
|
||||
await bind.sessionSetFlutterOption(
|
||||
sessionId: ffi.sessionId,
|
||||
k: kCustomScalePercentKey,
|
||||
v: v.toString());
|
||||
final curStyle = await bind.sessionGetViewStyle(sessionId: ffi.sessionId);
|
||||
if (curStyle != kRemoteViewStyleCustom) {
|
||||
await bind.sessionSetViewStyle(
|
||||
sessionId: ffi.sessionId, value: kRemoteViewStyleCustom);
|
||||
}
|
||||
await ffi.canvasModel.updateViewStyle();
|
||||
if (isMobile) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
onScaleChanged?.call(v);
|
||||
} catch (e, st) {
|
||||
debugPrint('[CustomScale] Apply failed: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
void nudgeScale(int delta) {
|
||||
final next = _clampScale(_scaleValue + delta);
|
||||
setState(() {
|
||||
_scaleValue = next;
|
||||
_scalePos = _mapPercentToPos(next);
|
||||
});
|
||||
onScaleChanged?.call(next);
|
||||
_debouncerScale.value = next;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncerScale.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onSliderChanged(double v) {
|
||||
final snapped = _snapNormalizedPos(v);
|
||||
final next = _mapPosToPercent(snapped);
|
||||
if (next != _scaleValue || snapped != _scalePos) {
|
||||
setState(() {
|
||||
_scalePos = snapped;
|
||||
_scaleValue = next;
|
||||
});
|
||||
onScaleChanged?.call(next);
|
||||
_debouncerScale.value = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,29 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:flutter_hbb/utils/http_service.dart' as http;
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import 'address_book.dart';
|
||||
|
||||
void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) {
|
||||
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
|
||||
'', dialogManager);
|
||||
void clientClose(SessionID sessionId, FFI ffi) async {
|
||||
if (allowAskForNoteAtEndOfConnection(ffi, true)) {
|
||||
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
|
||||
return;
|
||||
}
|
||||
closeConnection();
|
||||
} else {
|
||||
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
|
||||
'', ffi.dialogManager);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ValidationRule {
|
||||
@@ -819,23 +828,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 +864,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 +894,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 +964,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 +1186,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,
|
||||
),
|
||||
@@ -1468,56 +1518,71 @@ showSetOSAccount(
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildNoteTextField({
|
||||
required TextEditingController controller,
|
||||
required VoidCallback onEscape,
|
||||
}) {
|
||||
final focusNode = FocusNode(
|
||||
onKey: (FocusNode node, RawKeyEvent evt) {
|
||||
if (evt.logicalKey.keyLabel == 'Enter') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
int pos = controller.selection.base.offset;
|
||||
controller.text =
|
||||
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
||||
controller.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (evt.logicalKey.keyLabel == 'Esc') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
onEscape();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return TextField(
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: InputDecoration(
|
||||
hintText: translate('input note here'),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: EdgeInsets.all(12),
|
||||
),
|
||||
minLines: 5,
|
||||
maxLines: null,
|
||||
maxLength: 256,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
).workaroundFreezeLinuxMint();
|
||||
}
|
||||
|
||||
showAuditDialog(FFI ffi) async {
|
||||
final controller = TextEditingController(text: ffi.auditNote);
|
||||
final controller = TextEditingController(
|
||||
text: bind.sessionGetLastAuditNote(sessionId: ffi.sessionId));
|
||||
ffi.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
var text = controller.text;
|
||||
bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
|
||||
ffi.auditNote = text;
|
||||
close();
|
||||
}
|
||||
|
||||
late final focusNode = FocusNode(
|
||||
onKey: (FocusNode node, RawKeyEvent evt) {
|
||||
if (evt.logicalKey.keyLabel == 'Enter') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
int pos = controller.selection.base.offset;
|
||||
controller.text =
|
||||
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
||||
controller.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (evt.logicalKey.keyLabel == 'Esc') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
close();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Note')),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
height: 120,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: const InputDecoration.collapsed(
|
||||
hintText: 'input note here',
|
||||
),
|
||||
maxLines: null,
|
||||
maxLength: 256,
|
||||
child: buildNoteTextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
).workaroundFreezeLinuxMint()),
|
||||
onEscape: close,
|
||||
)),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit)
|
||||
@@ -1528,6 +1593,223 @@ showAuditDialog(FFI ffi) async {
|
||||
});
|
||||
}
|
||||
|
||||
bool allowAskForNoteAtEndOfConnection(FFI? ffi, bool closedByControlling) {
|
||||
if (ffi == null) {
|
||||
return false;
|
||||
}
|
||||
return mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection) &&
|
||||
bind
|
||||
.sessionGetAuditServerSync(sessionId: ffi.sessionId, typ: "conn")
|
||||
.isNotEmpty &&
|
||||
bind.sessionGetAuditGuid(sessionId: ffi.sessionId).isNotEmpty &&
|
||||
bind.sessionGetLastAuditNote(sessionId: ffi.sessionId).isEmpty &&
|
||||
(!closedByControlling ||
|
||||
bind.willSessionCloseCloseSession(sessionId: ffi.sessionId));
|
||||
}
|
||||
|
||||
// return value: close canceled
|
||||
// true: return
|
||||
// false: go on
|
||||
Future<bool> desktopTryShowTabAuditDialogCloseCancelled(
|
||||
{required String id, required DesktopTabController tabController}) async {
|
||||
try {
|
||||
final page =
|
||||
tabController.state.value.tabs.firstWhere((tab) => tab.key == id).page;
|
||||
final ffi = (page as dynamic).ffi;
|
||||
final res = await showConnEndAuditDialogCloseCanceled(ffi: ffi);
|
||||
return res;
|
||||
} catch (e) {
|
||||
debugPrint('Failed to show audit dialog: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// return value:
|
||||
// true: return
|
||||
// false: go on
|
||||
Future<bool> showConnEndAuditDialogCloseCanceled(
|
||||
{required FFI ffi, String? type, String? title, String? text}) async {
|
||||
final res = await _showConnEndAuditDialogCloseCanceled(
|
||||
ffi: ffi, type: type, title: title, text: text);
|
||||
if (res == true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// return value:
|
||||
// true: return
|
||||
// false / null: go on
|
||||
Future<bool?> _showConnEndAuditDialogCloseCanceled({
|
||||
required FFI ffi,
|
||||
String? type,
|
||||
String? title,
|
||||
String? text,
|
||||
}) async {
|
||||
final closedByControlling = type == null;
|
||||
final showDialog = allowAskForNoteAtEndOfConnection(ffi, closedByControlling);
|
||||
if (!showDialog) {
|
||||
return false;
|
||||
}
|
||||
ffi.dialogManager.dismissAll();
|
||||
|
||||
Future<void> updateAuditNoteByGuid(String auditGuid, String note) async {
|
||||
debugPrint('Updating audit note for GUID: $auditGuid, note: $note');
|
||||
try {
|
||||
final apiServer = await bind.mainGetApiServer();
|
||||
if (apiServer.isEmpty) {
|
||||
debugPrint('API server is empty, cannot update audit note');
|
||||
return;
|
||||
}
|
||||
final url = '$apiServer/api/audit';
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
final body = jsonEncode({
|
||||
'guid': auditGuid,
|
||||
'note': note,
|
||||
});
|
||||
|
||||
final response = await http.put(
|
||||
Uri.parse(url),
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint('Successfully updated audit note for GUID: $auditGuid');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Failed to update audit note. Status: ${response.statusCode}, Body: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error updating audit note: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final controller = TextEditingController();
|
||||
bool askForNote =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
final isOptFixed = isOptionFixed(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
bool isInProgress = false;
|
||||
|
||||
return await ffi.dialogManager.show<bool>((setState, close, context) {
|
||||
cancel() {
|
||||
close(true);
|
||||
}
|
||||
|
||||
set() async {
|
||||
if (isInProgress) return;
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
var text = controller.text;
|
||||
if (text.isNotEmpty) {
|
||||
await updateAuditNoteByGuid(
|
||||
bind.sessionGetAuditGuid(sessionId: ffi.sessionId), text)
|
||||
.timeout(const Duration(seconds: 6), onTimeout: () {
|
||||
debugPrint('updateAuditNoteByGuid timeout after 6s');
|
||||
});
|
||||
}
|
||||
// Save the "ask for note" preference
|
||||
if (!isOptFixed) {
|
||||
await mainSetLocalBoolOption(
|
||||
kOptionAllowAskForNoteAtEndOfConnection, askForNote);
|
||||
}
|
||||
}
|
||||
|
||||
submit() async {
|
||||
await set();
|
||||
close(false);
|
||||
}
|
||||
|
||||
final buttons = [
|
||||
dialogButton('OK', onPressed: isInProgress ? null : submit)
|
||||
];
|
||||
if (type == 'relay-hint' || type == 'relay-hint2') {
|
||||
buttons.add(dialogButton('Retry', onPressed: () async {
|
||||
await set();
|
||||
close(true);
|
||||
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, false);
|
||||
}));
|
||||
if (type == 'relay-hint2') {
|
||||
buttons.add(dialogButton('Connect via relay', onPressed: () async {
|
||||
await set();
|
||||
close(true);
|
||||
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, true);
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (closedByControlling) {
|
||||
buttons.add(dialogButton('Cancel',
|
||||
onPressed: isInProgress ? null : cancel, isOutline: true));
|
||||
}
|
||||
|
||||
Widget content;
|
||||
if (closedByControlling) {
|
||||
content = SelectionArea(
|
||||
child: msgboxContent(
|
||||
'info', 'Close', 'Are you sure to close the connection?'));
|
||||
} else {
|
||||
content =
|
||||
SelectionArea(child: msgboxContent(type, title ?? '', text ?? ''));
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: SizedBox(
|
||||
width: 350,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
content,
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: buildNoteTextField(
|
||||
controller: controller,
|
||||
onEscape: cancel,
|
||||
),
|
||||
),
|
||||
if (!isOptFixed) ...[
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
askForNote = !askForNote;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: askForNote,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
askForNote = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate('note-at-conn-end-tip'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isInProgress)
|
||||
const LinearProgressIndicator().marginOnly(top: 4),
|
||||
],
|
||||
)),
|
||||
actions: buttons,
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showConfirmSwitchSidesDialog(
|
||||
SessionID sessionId, String id, OverlayDialogManager dialogManager) async {
|
||||
dialogManager.show((setState, close, context) {
|
||||
@@ -1623,6 +1905,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) {
|
||||
@@ -1720,6 +2024,49 @@ void editAbTagDialog(
|
||||
});
|
||||
}
|
||||
|
||||
void editAbPeerNoteDialog(String id) {
|
||||
var isInProgress = false;
|
||||
final currentNote = gFFI.abModel.getPeerNote(id);
|
||||
var controller = TextEditingController(text: currentNote);
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
await gFFI.abModel.changeNote(id: id, note: controller.text);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Edit note")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
maxLines: 3,
|
||||
minLines: 1,
|
||||
maxLength: 300,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Note'),
|
||||
),
|
||||
).workaroundFreezeLinuxMint(),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
if (isInProgress) const LinearProgressIndicator(),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void renameDialog(
|
||||
{required String oldName,
|
||||
FormFieldValidator<String>? validator,
|
||||
@@ -2015,15 +2362,20 @@ void showWindowsSessionsDialog(
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
msgboxContent(type, title, text).marginOnly(bottom: 12),
|
||||
ComboBox(
|
||||
keys: sids,
|
||||
values: names,
|
||||
initialKey: selectedUserValue,
|
||||
onChanged: (value) {
|
||||
selectedUserValue = value;
|
||||
}),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ComboBox(
|
||||
keys: sids,
|
||||
values: names,
|
||||
initialKey: selectedUserValue,
|
||||
onChanged: (value) {
|
||||
selectedUserValue = value;
|
||||
}),
|
||||
dialogButton('Connect', onPressed: submit, isOutline: false),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/common/widgets/remote_input.dart';
|
||||
|
||||
enum GestureState {
|
||||
none,
|
||||
@@ -96,6 +97,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
if (onTwoFingerScaleEnd != null) {
|
||||
onTwoFingerScaleEnd!(d);
|
||||
}
|
||||
if (isSpecialHoldDragActive) {
|
||||
// If we are in special drag mode, we need to reset the state.
|
||||
// Otherwise, the next `onTwoFingerScaleUpdate()` will handle a wrong `focalPoint`.
|
||||
_currentState = GestureState.none;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
debugPrint("ThreeFingerState.vertical onEnd");
|
||||
|
||||
@@ -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) {
|
||||
@@ -397,6 +400,8 @@ Future<bool?> loginDialog() async {
|
||||
String? passwordMsg;
|
||||
var isInProgress = false;
|
||||
final RxString curOP = ''.obs;
|
||||
// Track hover state for the close icon
|
||||
bool isCloseHovered = false;
|
||||
|
||||
final loginOptions = [].obs;
|
||||
Future.delayed(Duration.zero, () async {
|
||||
@@ -455,10 +460,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;
|
||||
}
|
||||
}
|
||||
@@ -550,21 +559,27 @@ Future<bool?> loginDialog() async {
|
||||
Text(
|
||||
translate('Login'),
|
||||
).marginOnly(top: MyTheme.dialogPadding),
|
||||
InkWell(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
// No need to handle the branch of null.
|
||||
// Because we can ensure the color is not null when debug.
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.55),
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => isCloseHovered = true),
|
||||
onExit: (_) => setState(() => isCloseHovered = false),
|
||||
child: InkWell(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
// No need to handle the branch of null.
|
||||
// Because we can ensure the color is not null when debug.
|
||||
color: isCloseHovered
|
||||
? Colors.white
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.55),
|
||||
),
|
||||
onTap: onDialogCancel,
|
||||
hoverColor: Colors.red,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
onTap: onDialogCancel,
|
||||
hoverColor: Colors.red,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
).marginOnly(top: 10, right: 15),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -50,6 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
checkScreenSize: true,
|
||||
position: draggablePositions.chatWindow,
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -395,7 +396,10 @@ class _DraggableState extends State<Draggable> {
|
||||
_chatModel?.setChatWindowPosition(position);
|
||||
}
|
||||
|
||||
checkScreenSize() {}
|
||||
checkScreenSize() {
|
||||
// Ensure the draggable always stays within current screen bounds
|
||||
widget.position.tryAdjust(widget.width, widget.height, 1);
|
||||
}
|
||||
|
||||
checkKeyboard() {
|
||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
@@ -517,6 +521,12 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
_lastBottomHeight = bottomHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
position.tryAdjust(_width, _height, 1);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
checkKeyboard();
|
||||
|
||||
@@ -127,6 +127,10 @@ class _PeerCardState extends State<_PeerCard>
|
||||
);
|
||||
}
|
||||
|
||||
bool _showNote(Peer peer) {
|
||||
return peerTabShowNote(widget.tab) && peer.note.isNotEmpty;
|
||||
}
|
||||
|
||||
makeChild(bool isPortrait, Peer peer) {
|
||||
final name = hideUsernameOnCard == true
|
||||
? peer.hostname
|
||||
@@ -134,6 +138,8 @@ class _PeerCardState extends State<_PeerCard>
|
||||
final greyStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||
final showNote = _showNote(peer);
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
@@ -185,14 +191,44 @@ class _PeerCardState extends State<_PeerCard>
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
)),
|
||||
]).marginOnly(top: isPortrait ? 0 : 2),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
message: name,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showNote)
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: peer.note,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
peer.note,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).marginOnly(
|
||||
left: peerCardUiType.value ==
|
||||
PeerUiType.list
|
||||
? 32
|
||||
: 4),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
).marginOnly(top: 2),
|
||||
@@ -278,7 +314,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
padding: const EdgeInsets.all(6),
|
||||
child:
|
||||
getPlatformImage(peer.platform, size: 60),
|
||||
).marginOnly(top: 4),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -297,8 +333,26 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_showNote(peer))
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: peer.note,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: Text(
|
||||
peer.note,
|
||||
style: const TextStyle(
|
||||
color: Colors.white38,
|
||||
fontSize: 10),
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingAll(4.0),
|
||||
).paddingOnly(top: 4.0, left: 4.0, right: 4.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -491,6 +545,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 +554,9 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
if (isTerminalRunAsAdmin) {
|
||||
setEnvTerminalAdmin();
|
||||
}
|
||||
connectInPeerTab(
|
||||
context,
|
||||
peer,
|
||||
@@ -506,6 +565,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
isTerminal: isTerminal || isTerminalRunAsAdmin,
|
||||
);
|
||||
},
|
||||
padding: menuPadding,
|
||||
@@ -541,6 +601,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 +970,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 +1035,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 +1095,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 +1154,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));
|
||||
}
|
||||
@@ -1086,6 +1188,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty) {
|
||||
menuItems.add(_editTagAction(peer.id));
|
||||
}
|
||||
menuItems.add(_editNoteAction(peer.id));
|
||||
}
|
||||
final addressbooks = gFFI.abModel.addressBooksCanWrite();
|
||||
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
|
||||
@@ -1125,6 +1228,21 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _editNoteAction(String id) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Edit note'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
editAbPeerNoteDialog(id);
|
||||
},
|
||||
padding: super.menuPadding,
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
Future<String> _getAlias(String id) async =>
|
||||
@@ -1193,12 +1311,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 +1542,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) {
|
||||
@@ -1434,12 +1561,20 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
password = peer.password;
|
||||
isSharedPassword = true;
|
||||
}
|
||||
if (password.isEmpty) {
|
||||
final abPassword = gFFI.abModel.getdefaultSharedPassword();
|
||||
if (abPassword != null) {
|
||||
password = abPassword;
|
||||
isSharedPassword = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
connect(context, peer.id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTerminal: isTerminal,
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP);
|
||||
|
||||
@@ -71,10 +71,12 @@ class _PeersView extends StatefulWidget {
|
||||
final Peers peers;
|
||||
final PeerFilter? peerFilter;
|
||||
final PeerCardBuilder peerCardBuilder;
|
||||
final PeerTabIndex peerTabIndex;
|
||||
|
||||
const _PeersView(
|
||||
{required this.peers,
|
||||
required this.peerCardBuilder,
|
||||
required this.peerTabIndex,
|
||||
this.peerFilter,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
@@ -395,8 +397,8 @@ class _PeersViewState extends State<_PeersView>
|
||||
return peers;
|
||||
}
|
||||
searchText = searchText.toLowerCase();
|
||||
final matches =
|
||||
await Future.wait(peers.map((peer) => matchPeer(searchText, peer)));
|
||||
final matches = await Future.wait(
|
||||
peers.map((peer) => matchPeer(searchText, peer, widget.peerTabIndex)));
|
||||
final filteredList = List<Peer>.empty(growable: true);
|
||||
for (var i = 0; i < peers.length; i++) {
|
||||
if (matches[i]) {
|
||||
@@ -441,7 +443,10 @@ abstract class BasePeersView extends StatelessWidget {
|
||||
break;
|
||||
}
|
||||
return _PeersView(
|
||||
peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
|
||||
peers: peers,
|
||||
peerFilter: peerFilter,
|
||||
peerCardBuilder: peerCardBuilder,
|
||||
peerTabIndex: peerTabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,6 +506,7 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
Widget build(BuildContext context) {
|
||||
final widget = super.build(context);
|
||||
bind.mainLoadLanPeers();
|
||||
bind.mainDiscover();
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// For virtual mouse when using the mouse mode on mobile.
|
||||
// Special hold-drag mode: one finger holds a button (left/right button), another finger pans.
|
||||
// This flag is to override the scale gesture to a pan gesture.
|
||||
bool isSpecialHoldDragActive = false;
|
||||
// Cache the last focal point to calculate deltas in special hold-drag mode.
|
||||
Offset _lastSpecialHoldDragFocalPoint = Offset.zero;
|
||||
|
||||
class RawTouchGestureDetectorRegion extends StatefulWidget {
|
||||
final Widget child;
|
||||
final FFI ffi;
|
||||
@@ -97,6 +104,10 @@ class _RawTouchGestureDetectorRegionState
|
||||
bool _touchModePanStarted = false;
|
||||
Offset _doubleFinerTapPosition = Offset.zero;
|
||||
|
||||
// For mouse mode, we need to block the events when the cursor is in a blocked area.
|
||||
// So we need to cache the last tap down position.
|
||||
Offset? _lastTapDownPositionForMouseMode;
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
InputModel get inputModel => widget.inputModel;
|
||||
@@ -111,22 +122,36 @@ class _RawTouchGestureDetectorRegionState
|
||||
);
|
||||
}
|
||||
|
||||
bool isNotTouchBasedDevice() {
|
||||
return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
|
||||
}
|
||||
|
||||
// Mobile, mouse mode.
|
||||
// Check if should block the mouse tap event (`_lastTapDownPositionForMouseMode`).
|
||||
bool shouldBlockMouseModeEvent() {
|
||||
return _lastTapDownPositionForMouseMode != null &&
|
||||
ffi.cursorModel.shouldBlock(_lastTapDownPositionForMouseMode!.dx,
|
||||
_lastTapDownPositionForMouseMode!.dy);
|
||||
}
|
||||
|
||||
onTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
// Desktop or mobile "Touch mode"
|
||||
_lastTapDownDetails = d;
|
||||
} else {
|
||||
_lastTapDownPositionForMouseMode = d.localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
onTapUp(TapUpDetails d) async {
|
||||
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||
_lastTapDownDetails = null;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -142,10 +167,15 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onTap() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
|
||||
// Using `_lastTapDownPositionForMouseMode` instead.
|
||||
if (shouldBlockMouseModeEvent()) {
|
||||
return;
|
||||
}
|
||||
// Mobile, "Mouse mode"
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
@@ -153,17 +183,19 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onDoubleTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
} else {
|
||||
_lastTapDownPositionForMouseMode = d.localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleTap() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
||||
@@ -173,13 +205,19 @@ class _RawTouchGestureDetectorRegionState
|
||||
!ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
|
||||
return;
|
||||
}
|
||||
// Check if the position is in a blocked area when using the mouse mode.
|
||||
if (!handleTouch) {
|
||||
if (shouldBlockMouseModeEvent()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
|
||||
onLongPressDown(LongPressDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -194,11 +232,13 @@ class _RawTouchGestureDetectorRegionState
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
await inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
} else {
|
||||
_lastTapDownPositionForMouseMode = d.localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressUp() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -208,7 +248,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
// for mobiles
|
||||
onLongPress() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!ffi.ffiModel.isPeerMobile) {
|
||||
@@ -218,6 +258,10 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (!isMoved) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (shouldBlockMouseModeEvent()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await inputModel.tap(MouseButtons.right);
|
||||
} else {
|
||||
@@ -228,7 +272,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async {
|
||||
if (!ffiModel.isPeerMobile || lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (!ffiModel.isPeerMobile || isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -241,7 +285,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onDoubleFinerTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
_doubleFinerTapPosition = d.localPosition;
|
||||
@@ -250,7 +294,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onDoubleFinerTap(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -266,25 +310,27 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onHoldDragStart(DragStartDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
if (isSpecialHoldDragActive) return;
|
||||
await inputModel.sendMouse('down', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragUpdate(DragUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
if (isSpecialHoldDragActive) return;
|
||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragEnd(DragEndDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
@@ -296,7 +342,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||
_lastTapDownDetails = null;
|
||||
lastDeviceKind = d.kind ?? lastDeviceKind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
@@ -342,7 +388,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 +402,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onOneFingerPanEnd(DragEndDetails d) async {
|
||||
_touchModePanStarted = false;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop || isWebDesktop) {
|
||||
@@ -370,15 +416,29 @@ class _RawTouchGestureDetectorRegionState
|
||||
// scale + pan event
|
||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||
_lastTapDownDetails = null;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if (isSpecialHoldDragActive) {
|
||||
// Initialize the last focal point to calculate deltas manually.
|
||||
_lastSpecialHoldDragFocalPoint = d.focalPoint;
|
||||
}
|
||||
}
|
||||
|
||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If in special drag mode, perform a pan instead of a scale.
|
||||
if (isSpecialHoldDragActive) {
|
||||
// Calculate delta manually to avoid the jumpy behavior.
|
||||
final delta = d.focalPoint - _lastSpecialHoldDragFocalPoint;
|
||||
_lastSpecialHoldDragFocalPoint = d.focalPoint;
|
||||
await ffi.cursorModel.updatePan(delta * 2.0, d.focalPoint, handleTouch);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
final scale = ((d.scale - _scale) * 1000).toInt();
|
||||
_scale = d.scale;
|
||||
@@ -401,7 +461,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
|
||||
onTwoFingerScaleEnd(ScaleEndDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
@@ -416,7 +476,9 @@ class _RawTouchGestureDetectorRegionState
|
||||
// No idea why we need to set the view style to "" here.
|
||||
// bind.sessionSetViewStyle(sessionId: sessionId, value: "");
|
||||
}
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
if (!isSpecialHoldDragActive) {
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
get onHoldDragCancel => null;
|
||||
|
||||
@@ -230,7 +230,6 @@ List<(String, String)> otherDefaultSettings() {
|
||||
('Disable clipboard', kOptionDisableClipboard),
|
||||
('Lock after session end', kOptionLockAfterSessionEnd),
|
||||
('Privacy mode', kOptionPrivacyMode),
|
||||
if (isMobile) ('Touch mode', kOptionTouchMode),
|
||||
('True color (4:4:4)', kOptionI444),
|
||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||
@@ -243,8 +242,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(
|
||||
@@ -325,6 +363,11 @@ Future<List<TRadioMenu<String>>> toolbarViewStyle(
|
||||
child: Text(translate('Scale adaptive')),
|
||||
value: kRemoteViewStyleAdaptive,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Scale custom')),
|
||||
value: kRemoteViewStyleCustom,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -57,11 +58,14 @@ const String kWindowActionRebuild = "rebuild";
|
||||
const String kWindowEventHide = "hide";
|
||||
const String kWindowEventShow = "show";
|
||||
const String kWindowConnect = "connect";
|
||||
const String kWindowBumpMouse = "bump_mouse";
|
||||
|
||||
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";
|
||||
@@ -75,6 +79,7 @@ const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||
|
||||
const String kOptionViewStyle = "view_style";
|
||||
const String kOptionScrollStyle = "scroll_style";
|
||||
const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness";
|
||||
const String kOptionImageQuality = "image_quality";
|
||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionTextureRender = "use-texture-render";
|
||||
@@ -103,6 +108,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 +117,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 +148,31 @@ 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";
|
||||
const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
||||
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
||||
|
||||
// buildin opitons
|
||||
// network options
|
||||
const String kOptionAllowWebSocket = "allow-websocket";
|
||||
const String kOptionAllowInsecureTLSFallback = "allow-insecure-tls-fallback";
|
||||
const String kOptionDisableUdp = "disable-udp";
|
||||
const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
|
||||
|
||||
// builtin options
|
||||
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 =
|
||||
@@ -157,6 +181,7 @@ const kHideUsernameOnCard = "hide-username-on-card";
|
||||
const String kOptionHideHelpCards = "hide-help-cards";
|
||||
|
||||
const String kOptionToggleViewOnly = "view-only";
|
||||
const String kOptionToggleShowMyCursor = "show-my-cursor";
|
||||
|
||||
const String kOptionDisableFloatingWindow = "disable-floating-window";
|
||||
|
||||
@@ -220,7 +245,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';
|
||||
@@ -290,12 +322,18 @@ const kRemoteViewStyleOriginal = 'original';
|
||||
/// [kRemoteViewStyleAdaptive] Show remote image scaling by ratio factor.
|
||||
const kRemoteViewStyleAdaptive = 'adaptive';
|
||||
|
||||
/// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent.
|
||||
const kRemoteViewStyleCustom = 'custom';
|
||||
|
||||
/// [kRemoteScrollStyleAuto] Scroll image auto by position.
|
||||
const kRemoteScrollStyleAuto = 'scrollauto';
|
||||
|
||||
/// [kRemoteScrollStyleBar] Scroll image with scroll bar.
|
||||
const kRemoteScrollStyleBar = 'scrollbar';
|
||||
|
||||
/// [kRemoteScrollStyleEdge] Scroll image auto at edges.
|
||||
const kRemoteScrollStyleEdge = 'scrolledge';
|
||||
|
||||
/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode.
|
||||
const kScrollModeDefault = 'default';
|
||||
|
||||
@@ -316,6 +354,23 @@ const kRemoteImageQualityCustom = 'custom';
|
||||
|
||||
const kIgnoreDpi = true;
|
||||
|
||||
const Set<PointerDeviceKind> kTouchBasedDeviceKinds = {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
};
|
||||
|
||||
// Scale custom related constants
|
||||
const String kCustomScalePercentKey =
|
||||
'custom_scale_percent'; // Flutter option key for storing custom scale percent (integer 5-1000)
|
||||
const int kScaleCustomMinPercent = 5;
|
||||
const int kScaleCustomPivotPercent = 100; // 100% should be at 1/3 of track
|
||||
const int kScaleCustomMaxPercent = 1000;
|
||||
const double kScaleCustomPivotPos = 1.0 / 3.0; // first 1/3 → up to 100%
|
||||
const double kScaleCustomDetentEpsilon =
|
||||
0.006; // snap range around pivot (~0.6%)
|
||||
const Duration kDebounceCustomScaleDuration = Duration(milliseconds: 300);
|
||||
|
||||
// ================================ 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.
|
||||
@@ -369,6 +374,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
device_group_name: '',
|
||||
note: '',
|
||||
);
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
@@ -527,64 +533,74 @@ 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;
|
||||
await mod_menu
|
||||
.showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||
items: [
|
||||
(
|
||||
'Transfer file',
|
||||
() => onConnect(isFileTransfer: true)
|
||||
),
|
||||
(
|
||||
'View camera',
|
||||
() => onConnect(isViewCamera: true)
|
||||
),
|
||||
]
|
||||
.map((e) => MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate(e.$1),
|
||||
style: style,
|
||||
),
|
||||
proc: () => e.$2(),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: kDesktopMenuPadding.left),
|
||||
dismissOnClicked: true,
|
||||
))
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor:
|
||||
CustomPopupMenuTheme.commonColor,
|
||||
height: CustomPopupMenuTheme.height,
|
||||
dividerHeight: CustomPopupMenuTheme
|
||||
.dividerHeight)))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
elevation: 8,
|
||||
)
|
||||
.then((_) {
|
||||
_menuOpen.value = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
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,
|
||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||
items: [
|
||||
(
|
||||
'Transfer file',
|
||||
() => onConnect(isFileTransfer: true)
|
||||
),
|
||||
(
|
||||
'View camera',
|
||||
() => onConnect(isViewCamera: true)
|
||||
),
|
||||
(
|
||||
'${translate('Terminal')} (beta)',
|
||||
() => onConnect(isTerminal: true)
|
||||
),
|
||||
]
|
||||
.map((e) => MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) =>
|
||||
Text(
|
||||
translate(e.$1),
|
||||
style: style,
|
||||
),
|
||||
proc: () => e.$2(),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
kDesktopMenuPadding.left),
|
||||
dismissOnClicked: true,
|
||||
))
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: CustomPopupMenuTheme
|
||||
.commonColor,
|
||||
height:
|
||||
CustomPopupMenuTheme.height,
|
||||
dividerHeight:
|
||||
CustomPopupMenuTheme
|
||||
.dividerHeight)))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
elevation: 8,
|
||||
)
|
||||
.then((_) {
|
||||
_menuOpen.value = false;
|
||||
});
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -12,17 +12,18 @@ 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';
|
||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||
import 'package:get/get.dart';
|
||||
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 +434,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
updateUrl.isNotEmpty &&
|
||||
!isCardClosed &&
|
||||
bind.mainUriPrefixSync().contains('rustdesk')) {
|
||||
final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled();
|
||||
String btnText = isToUpdate ? 'Update' : '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, "", () {});
|
||||
@@ -750,9 +761,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
'scaleFactor': screen.scaleFactor,
|
||||
};
|
||||
|
||||
bool isChattyMethod(String methodName) {
|
||||
switch (methodName) {
|
||||
case kWindowBumpMouse: return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
debugPrint(
|
||||
if (!isChattyMethod(call.method)) {
|
||||
debugPrint(
|
||||
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
}
|
||||
if (call.method == kWindowMainWindowOnTop) {
|
||||
windowOnTop(null);
|
||||
} else if (call.method == kWindowGetWindowInfo) {
|
||||
@@ -776,12 +797,17 @@ 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'],
|
||||
forceRelay: call.arguments['forceRelay'],
|
||||
connToken: call.arguments['connToken'],
|
||||
);
|
||||
} else if (call.method == kWindowBumpMouse) {
|
||||
return RdPlatformChannel.instance.bumpMouse(
|
||||
dx: call.arguments['dx'],
|
||||
dy: call.arguments['dy']);
|
||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||
final args = call.arguments.split(',');
|
||||
int? windowId;
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/printer_model.dart';
|
||||
@@ -76,7 +77,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 +473,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,18 +528,45 @@ 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)) {
|
||||
children.add(_OptionCheckBox(
|
||||
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||
}
|
||||
children.add(_OptionCheckBox(
|
||||
context,
|
||||
'note-at-conn-end-tip',
|
||||
kOptionAllowAskForNoteAtEndOfConnection,
|
||||
isServer: false,
|
||||
));
|
||||
return _Card(title: 'Other', children: children);
|
||||
}
|
||||
|
||||
@@ -986,6 +1018,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 +1120,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 +1184,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
],
|
||||
),
|
||||
enabled: tmpEnabled && !locked),
|
||||
if (usePassword) numericOneTimePassword,
|
||||
if (usePassword) radios[1],
|
||||
if (usePassword)
|
||||
_SubButton('Set permanent password', setPasswordDialog,
|
||||
@@ -1466,11 +1529,90 @@ 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,
|
||||
);
|
||||
}
|
||||
|
||||
Widget switchWidget(IconData icon, String title, String tooltipMessage,
|
||||
String optionKey) =>
|
||||
listTile(
|
||||
icon: icon,
|
||||
title: title,
|
||||
showTooltip: true,
|
||||
tooltipMessage: tooltipMessage,
|
||||
trailing: Switch(
|
||||
value: mainGetBoolOptionSync(optionKey),
|
||||
onChanged: locked || isOptionFixed(optionKey)
|
||||
? null
|
||||
: (value) {
|
||||
mainSetBoolOption(optionKey, value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
|
||||
final divider = const Divider(height: 1, indent: 16, endIndent: 16);
|
||||
return _Card(
|
||||
title: 'Network',
|
||||
children: [
|
||||
@@ -1479,39 +1621,68 @@ 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,
|
||||
onTap: () => showServerSettings(gFFI.dialogManager),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
minLeadingWidth: 0,
|
||||
horizontalTitleGap: 10,
|
||||
listTile(
|
||||
icon: Icons.dns_outlined,
|
||||
title: 'ID/Relay Server',
|
||||
onTap: () => showServerSettings(gFFI.dialogManager, setState),
|
||||
),
|
||||
if (!hideServer && !hideProxy)
|
||||
Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (!hideProxy && !hideServer) divider,
|
||||
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),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
minLeadingWidth: 0,
|
||||
horizontalTitleGap: 10,
|
||||
),
|
||||
if (!hideWebSocket && (!hideServer || !hideProxy)) divider,
|
||||
if (!hideWebSocket)
|
||||
switchWidget(
|
||||
Icons.web_asset_outlined,
|
||||
'Use WebSocket',
|
||||
'${translate('websocket_tip')}\n\n${translate('server-oss-not-support-tip')}',
|
||||
kOptionAllowWebSocket),
|
||||
if (!isWeb)
|
||||
futureBuilder(
|
||||
future: bind.mainIsUsingPublicServer(),
|
||||
hasData: (isUsingPublicServer) {
|
||||
if (isUsingPublicServer) {
|
||||
return Offstage();
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
if (!hideServer || !hideProxy || !hideWebSocket)
|
||||
divider,
|
||||
switchWidget(
|
||||
Icons.no_encryption_outlined,
|
||||
'Allow insecure TLS fallback',
|
||||
'allow-insecure-tls-fallback-tip',
|
||||
kOptionAllowInsecureTLSFallback),
|
||||
if (!outgoingOnly) divider,
|
||||
if (!outgoingOnly)
|
||||
listTile(
|
||||
icon: Icons.lan_outlined,
|
||||
title: 'Disable UDP',
|
||||
showTooltip: true,
|
||||
tooltipMessage:
|
||||
'${translate('disable-udp-tip')}\n\n${translate('server-oss-not-support-tip')}',
|
||||
trailing: Switch(
|
||||
value: bind.mainGetOptionSync(
|
||||
key: kOptionDisableUdp) ==
|
||||
'Y',
|
||||
onChanged:
|
||||
locked || isOptionFixed(kOptionDisableUdp)
|
||||
? null
|
||||
: (value) async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionDisableUdp,
|
||||
value: value ? 'Y' : 'N');
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1537,6 +1708,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);
|
||||
@@ -1573,6 +1745,13 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle);
|
||||
|
||||
onEdgeScrollEdgeThicknessChanged(double value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: kOptionEdgeScrollEdgeThickness, value: value.round().toString());
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
return _Card(title: 'Default Scroll Style', children: [
|
||||
_Radio(context,
|
||||
value: kRemoteScrollStyleAuto,
|
||||
@@ -1584,6 +1763,23 @@ class _DisplayState extends State<_Display> {
|
||||
groupValue: groupValue,
|
||||
label: 'Scrollbar',
|
||||
onChanged: isOptFixed ? null : onChanged),
|
||||
if (!isWeb) ...[
|
||||
_Radio(context,
|
||||
value: kRemoteScrollStyleEdge,
|
||||
groupValue: groupValue,
|
||||
label: 'ScrollEdge',
|
||||
onChanged: isOptFixed ? null : onChanged),
|
||||
Offstage(
|
||||
offstage: groupValue != kRemoteScrollStyleEdge,
|
||||
child: EdgeThicknessControl(
|
||||
value: double.tryParse(bind.mainGetUserDefaultOption(
|
||||
key: kOptionEdgeScrollEdgeThickness)) ??
|
||||
100.0,
|
||||
onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness)
|
||||
? null
|
||||
: onEdgeScrollEdgeThicknessChanged,
|
||||
)),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1624,6 +1820,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 +2124,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 +2195,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 +2204,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,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
@@ -52,7 +53,7 @@ enum MouseFocusScope {
|
||||
}
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
const FileManagerPage(
|
||||
FileManagerPage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
@@ -67,9 +68,16 @@ class FileManagerPage extends StatefulWidget {
|
||||
final bool? forceRelay;
|
||||
final String? connToken;
|
||||
final DesktopTabController? tabController;
|
||||
final SimpleWrapper<State<FileManagerPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _FileManagerPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
State<StatefulWidget> createState() {
|
||||
final state = _FileManagerPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _FileManagerPageState extends State<FileManagerPage>
|
||||
@@ -139,12 +147,26 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
}
|
||||
|
||||
Widget willPopScope(Widget child) {
|
||||
if (isWeb) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(_ffi.sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Overlay(key: _overlayKeyState.key, initialEntries: [
|
||||
OverlayEntry(builder: (_) {
|
||||
return Scaffold(
|
||||
return willPopScope(Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Row(
|
||||
children: [
|
||||
@@ -160,7 +182,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Flexible(flex: 2, child: statusList())
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
|
||||
@@ -40,7 +41,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
label: params['id'],
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(params['id']),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: params['id'],
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(params['id']);
|
||||
},
|
||||
page: FileManagerPage(
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
@@ -69,7 +78,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: FileManagerPage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
@@ -132,6 +149,14 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.state.value.tabs.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
|
||||
@@ -25,7 +25,7 @@ class _PortForward {
|
||||
}
|
||||
|
||||
class PortForwardPage extends StatefulWidget {
|
||||
const PortForwardPage({
|
||||
PortForwardPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
@@ -42,9 +42,16 @@ class PortForwardPage extends StatefulWidget {
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final SimpleWrapper<State<PortForwardPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _PortForwardPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||
State<PortForwardPage> createState() {
|
||||
final state = _PortForwardPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _PortForwardPageState extends State<PortForwardPage>
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
@@ -72,7 +73,10 @@ class RemotePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RemotePageState extends State<RemotePage>
|
||||
with AutomaticKeepAliveClientMixin, MultiWindowListener {
|
||||
with
|
||||
AutomaticKeepAliveClientMixin,
|
||||
MultiWindowListener,
|
||||
TickerProviderStateMixin {
|
||||
Timer? _timer;
|
||||
String keyboardMode = "legacy";
|
||||
bool _isWindowBlur = false;
|
||||
@@ -112,11 +116,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi = FFI(widget.sessionId);
|
||||
Get.put<FFI>(_ffi, tag: widget.id);
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
_ffi.canvasModel.activateLocalCursor();
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
_ffi.recordingModel
|
||||
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
||||
});
|
||||
_ffi.canvasModel.initializeEdgeScrollFallback(this);
|
||||
_ffi.start(
|
||||
widget.id,
|
||||
password: widget.password,
|
||||
@@ -395,7 +401,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
super.build(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, _ffi.dialogManager);
|
||||
clientClose(sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: MultiProvider(providers: [
|
||||
@@ -408,6 +414,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void enterView(PointerEnterEvent evt) {
|
||||
_ffi.canvasModel.rearmEdgeScroll();
|
||||
|
||||
_cursorOverImage.value = true;
|
||||
_firstEnterImage.value = true;
|
||||
if (_onEnterOrLeaveImage4Toolbar != null) {
|
||||
@@ -427,6 +435,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void leaveView(PointerExitEvent evt) {
|
||||
_ffi.canvasModel.disableEdgeScroll();
|
||||
|
||||
if (_ffi.ffiModel.keyboard) {
|
||||
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
|
||||
}
|
||||
@@ -625,7 +635,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
onHover: (evt) {},
|
||||
child: child);
|
||||
});
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
@@ -680,9 +690,20 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
|
||||
Widget _buildScrollAutoNonTextureRender(
|
||||
ImageModel m, CanvasModel c, double s) {
|
||||
double sizeScale = s;
|
||||
if (widget.ffi.ffiModel.isPeerLinux) {
|
||||
final displays = widget.ffi.ffiModel.pi.getCurDisplays();
|
||||
if (displays.isNotEmpty) {
|
||||
sizeScale = s / displays[0].scale;
|
||||
}
|
||||
}
|
||||
return CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
painter: ImagePainter(
|
||||
image: m.image,
|
||||
x: c.x / sizeScale,
|
||||
y: c.y / sizeScale,
|
||||
scale: sizeScale),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -695,17 +716,19 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
if (rect == null) {
|
||||
return Container();
|
||||
}
|
||||
final isPeerLinux = ffiModel.isPeerLinux;
|
||||
final curDisplay = ffiModel.pi.currentDisplay;
|
||||
for (var i = 0; i < displays.length; i++) {
|
||||
final textureId = widget.ffi.textureModel
|
||||
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
|
||||
if (true) {
|
||||
// both "textureId.value != -1" and "true" seems ok
|
||||
final sizeScale = isPeerLinux ? s / displays[i].scale : s;
|
||||
children.add(Positioned(
|
||||
left: (displays[i].x - rect.left) * s + offset.dx,
|
||||
top: (displays[i].y - rect.top) * s + offset.dy,
|
||||
width: displays[i].width * s,
|
||||
height: displays[i].height * s,
|
||||
width: displays[i].width * sizeScale,
|
||||
height: displays[i].height * sizeScale,
|
||||
child: Obx(() => Texture(
|
||||
textureId: textureId.value,
|
||||
filterQuality:
|
||||
|
||||
@@ -80,7 +80,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
label: peerId!,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(peerId),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: peerId!,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(peerId!);
|
||||
},
|
||||
page: RemotePage(
|
||||
key: ValueKey(peerId),
|
||||
id: peerId!,
|
||||
@@ -146,16 +154,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
String msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
String msgConn = getConnectionText(
|
||||
secure, direct, connectionType.stream_type.value);
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
if (fingerprint.isEmpty) {
|
||||
@@ -324,7 +324,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
translate('Close'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
proc: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(key);
|
||||
cancelFunc();
|
||||
},
|
||||
@@ -377,6 +383,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
@@ -431,7 +445,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
159
flutter/lib/desktop/pages/terminal_page.dart
Normal file
159
flutter/lib/desktop/pages/terminal_page.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
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 {
|
||||
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;
|
||||
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
|
||||
|
||||
@override
|
||||
State<TerminalPage> createState() {
|
||||
final state = _TerminalPageState();
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
double? _cellHeight;
|
||||
|
||||
@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}');
|
||||
|
||||
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
||||
_cellHeight = ph * 1.0;
|
||||
|
||||
// Schedule the setState for the next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// This method ensures that the number of visible rows is an integer by computing the
|
||||
// extra space left after dividing the available height by the height of a single
|
||||
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||
EdgeInsets _calculatePadding(double heightPx) {
|
||||
if (_cellHeight == null) {
|
||||
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||||
}
|
||||
final rows = (heightPx / _cellHeight!).floor();
|
||||
final extraSpace = heightPx - rows * _cellHeight!;
|
||||
final topBottom = extraSpace / 2.0;
|
||||
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final heightPx = constraints.maxHeight;
|
||||
return TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 0.7,
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
445
flutter/lib/desktop/pages/terminal_tab_page.dart
Normal file
445
flutter/lib/desktop/pages/terminal_tab_page.dart
Normal file
@@ -0,0 +1,445 @@
|
||||
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/common/widgets/dialog.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';
|
||||
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||
final tabLabel =
|
||||
alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
|
||||
return TabInfo(
|
||||
key: tabKey,
|
||||
label: tabLabel,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabKey,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
// Close the terminal session first
|
||||
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
||||
if (ffi != null) {
|
||||
final terminalModel = ffi.terminalModels[terminalId];
|
||||
if (terminalModel != null) {
|
||||
await terminalModel.closeTerminal();
|
||||
}
|
||||
}
|
||||
// Then close the tab
|
||||
tabController.closeBy(tabKey);
|
||||
},
|
||||
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) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,7 +360,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
super.build(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
clientClose(sessionId, _ffi.dialogManager);
|
||||
clientClose(sessionId, _ffi);
|
||||
return false;
|
||||
},
|
||||
child: MultiProvider(providers: [
|
||||
@@ -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;
|
||||
@@ -529,7 +527,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
|
||||
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
|
||||
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -79,7 +80,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
label: peerId!,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(peerId),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: peerId!,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(peerId!);
|
||||
},
|
||||
page: ViewCameraPage(
|
||||
key: ValueKey(peerId),
|
||||
id: peerId!,
|
||||
@@ -145,16 +154,8 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
String msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
String msgConn = getConnectionText(
|
||||
secure, direct, connectionType.stream_type.value);
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
if (fingerprint.isEmpty) {
|
||||
@@ -295,7 +296,13 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
translate('Close'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
proc: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(key);
|
||||
cancelFunc();
|
||||
},
|
||||
@@ -348,6 +355,14 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.length;
|
||||
if (connLength == 1) {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: tabController.state.value.tabs[0].key,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
@@ -401,7 +416,15 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
onTabCloseButton: () async {
|
||||
if (await desktopTryShowTabAuditDialogCloseCancelled(
|
||||
id: id,
|
||||
tabController: tabController,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
tabController.closeBy(id);
|
||||
},
|
||||
page: ViewCameraPage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user