mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-21 09:08:35 +08:00
Compare commits
939 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
914da2b86f | ||
|
|
582db9d542 | ||
|
|
9a1fd1aa4f | ||
|
|
58ddac63d2 | ||
|
|
a8eff641b2 | ||
|
|
d723c10a3b | ||
|
|
bf5abdb520 | ||
|
|
0f44de7dc3 | ||
|
|
4f1a4dc6a5 | ||
|
|
8c108065eb | ||
|
|
5fdcc748e1 | ||
|
|
bf03156dd9 | ||
|
|
964c2ed2b5 | ||
|
|
e942c80afb | ||
|
|
d7dcb5feab | ||
|
|
39d41486d6 | ||
|
|
b85526ce54 | ||
|
|
2a0c081380 | ||
|
|
1db4236f23 | ||
|
|
6749595afe | ||
|
|
e65c43a292 | ||
|
|
00b13bf918 | ||
|
|
f7f3bc8bee | ||
|
|
51c603a3a6 | ||
|
|
5fb026b8d5 | ||
|
|
f7530b16b8 | ||
|
|
db47209362 | ||
|
|
f375cbd871 | ||
|
|
0474c8fb03 | ||
|
|
c00d23846a | ||
|
|
a3d5ea8fb8 | ||
|
|
7525a6ed6a | ||
|
|
dd93416cf7 | ||
|
|
73d429d064 | ||
|
|
4bf3764b5d | ||
|
|
236687ae53 | ||
|
|
0d708f64b9 | ||
|
|
222c9de19f | ||
|
|
72fc34cd40 | ||
|
|
3f7244f23f | ||
|
|
dc24868800 | ||
|
|
1f557888f5 | ||
|
|
16db977fd8 | ||
|
|
a19d4d6686 | ||
|
|
633076ddd4 | ||
|
|
5581248a1e | ||
|
|
a4393b8f90 | ||
|
|
406d8469d8 | ||
|
|
0cf4711515 | ||
|
|
a2ba50c4ff | ||
|
|
c97cc15c0e | ||
|
|
750f1a1884 | ||
|
|
2cf83b41cc | ||
|
|
1f16b5236b | ||
|
|
dd4b5349cb | ||
|
|
46b4e21e8c | ||
|
|
3899e4e12e | ||
|
|
f016281e30 | ||
|
|
1d755c705b | ||
|
|
a0d79dd26d | ||
|
|
e6734af64e | ||
|
|
0d8e3dc24f | ||
|
|
0e44aa1ada | ||
|
|
9f40a0b490 | ||
|
|
23430bee97 | ||
|
|
04691a3b6c | ||
|
|
db0f65eedd | ||
|
|
2118b6dd7d | ||
|
|
71d7398ae7 | ||
|
|
2e16a2be56 | ||
|
|
ed24f432c3 | ||
|
|
669e8d5f8e | ||
|
|
016f4abb32 | ||
|
|
b1a946ec20 | ||
|
|
2576b46f34 | ||
|
|
a42df9a27b | ||
|
|
48102e9c53 | ||
|
|
5770aeee26 | ||
|
|
cb102a5a61 | ||
|
|
28f01784c1 | ||
|
|
29eefbcc25 | ||
|
|
8679e55f6a | ||
|
|
846db4e689 | ||
|
|
68afb89b99 | ||
|
|
804f035a87 | ||
|
|
267342e7e6 | ||
|
|
9e3633f1e4 | ||
|
|
06b3894249 | ||
|
|
d39129887b | ||
|
|
e5379bb073 | ||
|
|
c8dded1108 | ||
|
|
44e6b7dbb0 | ||
|
|
80857c22c9 | ||
|
|
6548f9f0ed | ||
|
|
58fd8780e5 | ||
|
|
0cccbc438b | ||
|
|
558e7cb5ac | ||
|
|
286181ca04 | ||
|
|
ce0fc14a8a | ||
|
|
ebfbc8ce61 | ||
|
|
fdb038c7c9 | ||
|
|
467e6dfd16 | ||
|
|
8c40c28fe0 | ||
|
|
9e25049fe8 | ||
|
|
e170153090 | ||
|
|
99e8753629 | ||
|
|
d5b0829065 | ||
|
|
2c8a60e0ea | ||
|
|
352de75fdd | ||
|
|
f7b35defc9 | ||
|
|
e471c01269 | ||
|
|
5dfaa10709 | ||
|
|
7506f94a53 | ||
|
|
a2d08fa40d | ||
|
|
159c883bf3 | ||
|
|
396d7680d3 | ||
|
|
7e93a5d3cf | ||
|
|
955c55b6cc | ||
|
|
b2b30b0cf5 | ||
|
|
6352b3a594 | ||
|
|
5be684d2e5 | ||
|
|
8dda17d546 | ||
|
|
0f5aaac1f5 | ||
|
|
35fa75f9c9 | ||
|
|
19d43b3f62 | ||
|
|
2b4a72897e | ||
|
|
cdc31b7fc7 | ||
|
|
f4ef1455c4 | ||
|
|
fe94512dd1 | ||
|
|
ae25542ce9 | ||
|
|
6c030a5230 | ||
|
|
a145d6ebcc | ||
|
|
14676e9618 | ||
|
|
72e8476ded | ||
|
|
5888b051d9 | ||
|
|
215cf73072 | ||
|
|
7f7d4a77b6 | ||
|
|
7e3a0c4ded | ||
|
|
89150317e1 | ||
|
|
f47faa548b | ||
|
|
36ed8f3f73 | ||
|
|
ca1ca21cf8 | ||
|
|
c7df539b31 | ||
|
|
e873188775 | ||
|
|
6953efc2d8 | ||
|
|
3e78aacc7e | ||
|
|
20b4ce3213 | ||
|
|
4b581c385c | ||
|
|
bdf6f5c3b9 | ||
|
|
6d0953dca4 | ||
|
|
fc0fc5ea10 | ||
|
|
f7f2d84e1f | ||
|
|
57acadd52a | ||
|
|
26cebd2aeb | ||
|
|
9a652e789d | ||
|
|
f6509e3fd6 | ||
|
|
445fe6e714 | ||
|
|
dc8a70bb26 | ||
|
|
adf9b82d6b | ||
|
|
be212b5186 | ||
|
|
59ba461d83 | ||
|
|
004c2e069c | ||
|
|
c168b7e979 | ||
|
|
79411430a5 | ||
|
|
853d655a92 | ||
|
|
2d5fa9ebbf | ||
|
|
b2deab08ab | ||
|
|
8452a17d79 | ||
|
|
19851c8a47 | ||
|
|
6b8ec6ae16 | ||
|
|
d648e67eaf | ||
|
|
ebb14af488 | ||
|
|
80afa98d66 | ||
|
|
2797a28c0d | ||
|
|
03c8d94024 | ||
|
|
c061eddf2a | ||
|
|
2e146190e1 | ||
|
|
fc110c4988 | ||
|
|
6003003228 | ||
|
|
70e5bee519 | ||
|
|
4062d1920c | ||
|
|
50b81c2356 | ||
|
|
00fe3a76c8 | ||
|
|
d11a3b9683 | ||
|
|
7934fa24a2 | ||
|
|
f0d9dee6ce | ||
|
|
3a82bddcd3 | ||
|
|
ebe044aee8 | ||
|
|
674305ce29 | ||
|
|
b9a5c6ff31 | ||
|
|
bcb0a7822a | ||
|
|
93aac0bc99 | ||
|
|
7fe58afa9c | ||
|
|
9d0f8d9886 | ||
|
|
aa7d0471db | ||
|
|
6cd9227e8d | ||
|
|
213ef58959 | ||
|
|
2fa1124752 | ||
|
|
d00a0d6c75 | ||
|
|
2e225eb84f | ||
|
|
3c57ee89c4 | ||
|
|
b6f870ac5f | ||
|
|
570ce7d28d | ||
|
|
3958090e0f | ||
|
|
99ed1b729e | ||
|
|
d16db77b52 | ||
|
|
56c176add4 | ||
|
|
42a856c24a | ||
|
|
b662090d64 | ||
|
|
29e50a5f91 | ||
|
|
7885e07eed | ||
|
|
65fd682f2a | ||
|
|
cdecbb0857 | ||
|
|
68ee781f26 | ||
|
|
0f307e7ca2 | ||
|
|
7ac49287df | ||
|
|
83faf6025b | ||
|
|
4faf5e6d3e | ||
|
|
d300596ac1 | ||
|
|
a30dc5988e | ||
|
|
e43a0da9a6 | ||
|
|
83b74c0930 | ||
|
|
0d2a39267f | ||
|
|
f87f380bb0 | ||
|
|
ab28acd709 | ||
|
|
04b53f8e1e | ||
|
|
eb0e5c2de3 | ||
|
|
8cb9b910a6 | ||
|
|
bdac79c91b | ||
|
|
88081fc3c4 | ||
|
|
6be451e9f3 | ||
|
|
64ed25879c | ||
|
|
d66e087d94 | ||
|
|
b7b0a44c52 | ||
|
|
44cba7adf7 | ||
|
|
5a8a64d284 | ||
|
|
eac6835982 | ||
|
|
7992ca4c85 | ||
|
|
ead200ea5d | ||
|
|
e146c2606f | ||
|
|
db158e1ffe | ||
|
|
72ef3c3394 | ||
|
|
e17378c6b3 | ||
|
|
0e05f5305c | ||
|
|
58f43da23e | ||
|
|
4a9e7f29da | ||
|
|
1589209567 | ||
|
|
5c6017b0a9 | ||
|
|
4246fe5b92 | ||
|
|
c441d2f03f | ||
|
|
6c21529594 | ||
|
|
6fc4253d46 | ||
|
|
7a2590d1f9 | ||
|
|
4cbbb5b64f | ||
|
|
f11104fcb5 | ||
|
|
19e8ca6c06 | ||
|
|
13005e8242 | ||
|
|
d0dde6e572 | ||
|
|
5e103a5ccb | ||
|
|
0c4e6f0a8c | ||
|
|
03a89a5cdd | ||
|
|
e22d13be1f | ||
|
|
ef9c4d96b7 | ||
|
|
bb7300a055 | ||
|
|
2d456fd1fc | ||
|
|
97108e788f | ||
|
|
b8f7259fbd | ||
|
|
2de1c62daf | ||
|
|
f5d2eff8d3 | ||
|
|
dbcf1fdb6f | ||
|
|
bd81e4d0fb | ||
|
|
80786f692c | ||
|
|
177edfea3e | ||
|
|
b4b08db778 | ||
|
|
8abf2f768c | ||
|
|
8b14cd5aea | ||
|
|
84a21d72b9 | ||
|
|
fa8e0ed27b | ||
|
|
b6e403a3fb | ||
|
|
3d368f9aca | ||
|
|
414ac950e0 | ||
|
|
34fa82f72c | ||
|
|
4d5fdc4725 | ||
|
|
1c4f28e708 | ||
|
|
7bf555ce4c | ||
|
|
bb388628a7 | ||
|
|
a2920578a1 | ||
|
|
b41a73f08d | ||
|
|
c6ace470e3 | ||
|
|
cdbe79d3c1 | ||
|
|
493ec07ff2 | ||
|
|
ec15412755 | ||
|
|
191b6d5b4e | ||
|
|
8718152dba | ||
|
|
c585db7516 | ||
|
|
515c91a407 | ||
|
|
b615029304 | ||
|
|
3b0f4b1f82 | ||
|
|
4a2bf7a0c3 | ||
|
|
79212f8ef7 | ||
|
|
c57a596156 | ||
|
|
948db1451f | ||
|
|
06aa18bfab | ||
|
|
f076afb6e1 | ||
|
|
582402f4d9 | ||
|
|
0e04ff506b | ||
|
|
e9f757dccb | ||
|
|
f02d858646 | ||
|
|
3727cd401c | ||
|
|
f0c9deca4b | ||
|
|
f41a8bc355 | ||
|
|
3100930136 | ||
|
|
9abafafcdd | ||
|
|
0d127dff3b | ||
|
|
83dbf88ffb | ||
|
|
3a6f56ebbc | ||
|
|
3e8c5d3b79 | ||
|
|
2aaca0c54a | ||
|
|
c6d587f0c7 | ||
|
|
c85682de8d | ||
|
|
9c12c3ee4e | ||
|
|
2c432dbf4d | ||
|
|
0c8643837f | ||
|
|
3fd97f7e60 | ||
|
|
5064696480 | ||
|
|
6f0fbd1088 | ||
|
|
b535722421 | ||
|
|
60e96b637d | ||
|
|
7f29c7a601 | ||
|
|
060dfedb74 | ||
|
|
2907ed6029 | ||
|
|
5649fcc9ca | ||
|
|
85ddfc0739 | ||
|
|
7c98bfd363 | ||
|
|
c23f377039 | ||
|
|
af89fb52f1 | ||
|
|
98f56fd506 | ||
|
|
88ea1ea85b | ||
|
|
2a1e33f95c | ||
|
|
2fe55e2b55 | ||
|
|
d6f5887b1d | ||
|
|
dce045461a | ||
|
|
4ab0324bc9 | ||
|
|
b2bb69fe42 | ||
|
|
ad96f9aacd | ||
|
|
9847fc5c77 | ||
|
|
3f3bca753c | ||
|
|
c223d6a87d | ||
|
|
14a428467c | ||
|
|
efc84e53ec | ||
|
|
f9ee0189f0 | ||
|
|
d90580943f | ||
|
|
f6484b4e2b | ||
|
|
0b5f48b926 | ||
|
|
f2f649319d | ||
|
|
35eaa94228 | ||
|
|
e09a050d41 | ||
|
|
38182ba020 | ||
|
|
7960b2ca10 | ||
|
|
722c789448 | ||
|
|
faa4c36956 | ||
|
|
d3a33d786d | ||
|
|
1a2d60dbfc | ||
|
|
2aa5e685b8 | ||
|
|
512c7df37d | ||
|
|
57fddf1c85 | ||
|
|
afe4206782 | ||
|
|
02bc5e3111 | ||
|
|
5a51284550 | ||
|
|
2d995826ad | ||
|
|
92f3bf2999 | ||
|
|
bfe282c2db | ||
|
|
2d0b013af3 | ||
|
|
3df938ed61 | ||
|
|
d4b6cab742 | ||
|
|
8d327af0ac | ||
|
|
f135fbee26 | ||
|
|
d65977bd59 | ||
|
|
1e0242ede9 | ||
|
|
839d3365f8 | ||
|
|
182ee8f233 | ||
|
|
24eb6f8c38 | ||
|
|
13bade262f | ||
|
|
d2ff675fbc | ||
|
|
1905a81f9a | ||
|
|
e2382a1465 | ||
|
|
ec00cd2ae0 | ||
|
|
e0fad4eaa5 | ||
|
|
882b39b067 | ||
|
|
d15a641d88 | ||
|
|
90ac8b7b0b | ||
|
|
d64afdcff1 | ||
|
|
eb58a39f57 | ||
|
|
d12352d568 | ||
|
|
217a9808b2 | ||
|
|
1b08adb178 | ||
|
|
f113fda6d4 | ||
|
|
5a6a773583 | ||
|
|
627d21a00a | ||
|
|
44e13c84bc | ||
|
|
b96067cf2e | ||
|
|
b2fe5cce5e | ||
|
|
55775adba6 | ||
|
|
0500f56163 | ||
|
|
52e5d519ab | ||
|
|
bdfbb97bf8 | ||
|
|
cff117cb40 | ||
|
|
cf71adc7e1 | ||
|
|
0582f2a168 | ||
|
|
2721af3052 | ||
|
|
f9950639e8 | ||
|
|
8f8c9ddc25 | ||
|
|
58596ad1ab | ||
|
|
b17ea991a3 | ||
|
|
45d70f73bd | ||
|
|
7a6e483171 | ||
|
|
1792dd8d2c | ||
|
|
a4ea4b138f | ||
|
|
e2c6aedddd | ||
|
|
b91a03b602 | ||
|
|
27716f077e | ||
|
|
eaa054e599 | ||
|
|
e57d07f7d6 | ||
|
|
895831f46f | ||
|
|
6b16d7ee8a | ||
|
|
45dbea57f1 | ||
|
|
6ea0dbc70f | ||
|
|
30a5041799 | ||
|
|
35083724f6 | ||
|
|
959d5dd9c2 | ||
|
|
a24a261c36 | ||
|
|
75feb90b19 | ||
|
|
fd3cb1b0eb | ||
|
|
1902134f03 | ||
|
|
cce48c5030 | ||
|
|
0654b8e427 | ||
|
|
7df10b20a4 | ||
|
|
dbde5af545 | ||
|
|
85b4f0638f | ||
|
|
94f2751902 | ||
|
|
ffdfef0adc | ||
|
|
de7652836b | ||
|
|
59913bc6f3 | ||
|
|
d77a684b64 | ||
|
|
529c075fe1 | ||
|
|
5190589653 | ||
|
|
cb9a1b4bef | ||
|
|
a0beba4e20 | ||
|
|
d794425f2c | ||
|
|
1ba11fc3ef | ||
|
|
0dfb1ae776 | ||
|
|
c8a054f997 | ||
|
|
30c4e9cf00 | ||
|
|
c9fd2fc1c2 | ||
|
|
e9e3a436eb | ||
|
|
b9bd79895f | ||
|
|
36b574a5f8 | ||
|
|
fe90372b2f | ||
|
|
d47ddac94c | ||
|
|
854c374e2f | ||
|
|
7bafe142ca | ||
|
|
14ec8c4bea | ||
|
|
e9f2a77bf6 | ||
|
|
dcb4ce8d9a | ||
|
|
2702466cdf | ||
|
|
2016000d37 | ||
|
|
be0516c06c | ||
|
|
332a3635de | ||
|
|
9df4fa6a0e | ||
|
|
ddcb9ff819 | ||
|
|
7e9f5509ca | ||
|
|
c596d75e8c | ||
|
|
a0337d399c | ||
|
|
f0f52d7244 | ||
|
|
d55d796c00 | ||
|
|
6da36b27aa | ||
|
|
b4d3fc393e | ||
|
|
c4b68280fd | ||
|
|
97aeee7172 | ||
|
|
8bf57f1293 | ||
|
|
4584cebad5 | ||
|
|
557773144b | ||
|
|
32a29a5556 | ||
|
|
9f4a844c9b | ||
|
|
82b7650458 | ||
|
|
b7121c4447 | ||
|
|
b522de3b56 | ||
|
|
eb94caf4b6 | ||
|
|
0f59e0c950 | ||
|
|
05a254c682 | ||
|
|
576ba2c745 | ||
|
|
3dafda6ace | ||
|
|
2d048a3295 | ||
|
|
f2d345e7b1 | ||
|
|
6a0db02230 | ||
|
|
93f271912e | ||
|
|
55b78727ab | ||
|
|
69b555c059 | ||
|
|
07f327d641 | ||
|
|
c69f4ba4a2 | ||
|
|
d7e8d4d5c3 | ||
|
|
679a026e72 | ||
|
|
0db180a54f | ||
|
|
6c2de53e07 | ||
|
|
af132aa62e | ||
|
|
d528fd3762 | ||
|
|
663d355a48 | ||
|
|
27112e3480 | ||
|
|
482d3ff352 | ||
|
|
a444d4af94 | ||
|
|
6f77fda6fa | ||
|
|
8f7d653e80 | ||
|
|
e474b595ad | ||
|
|
b4c6292397 | ||
|
|
4aa994827c | ||
|
|
a521ccde47 | ||
|
|
03d280054b | ||
|
|
c0a3279856 | ||
|
|
e8643632bf | ||
|
|
6d8272472a | ||
|
|
409d5b124a | ||
|
|
0c1f3d056a | ||
|
|
0e702d8693 | ||
|
|
b155cd9a5a | ||
|
|
6a281fb7ba | ||
|
|
203891c957 | ||
|
|
1365df898f | ||
|
|
a2bc02b4c5 | ||
|
|
0d01aa4eea | ||
|
|
017c73132c | ||
|
|
534bfad50f | ||
|
|
429f44eb6a | ||
|
|
346c88b661 | ||
|
|
c49853e7b4 | ||
|
|
11232c6f23 | ||
|
|
9aa788f086 | ||
|
|
c35268c493 | ||
|
|
df0ad4486e | ||
|
|
d369385657 | ||
|
|
059e067bdc | ||
|
|
050759c1c2 | ||
|
|
6d264b4394 | ||
|
|
284330ed5f | ||
|
|
49205b604c | ||
|
|
c14c27970f | ||
|
|
b1682d5794 | ||
|
|
d0b9dd9ae7 | ||
|
|
0a94b7473d | ||
|
|
587a928b84 | ||
|
|
48dbc06b29 | ||
|
|
967515a34f | ||
|
|
8e0bce4da4 | ||
|
|
01078bd7b2 | ||
|
|
a97fbb2d48 | ||
|
|
6782f92703 | ||
|
|
9096c29fef | ||
|
|
100967c57b | ||
|
|
c871af2711 | ||
|
|
faf99ffe14 | ||
|
|
58d073b516 | ||
|
|
52a4d41c6f | ||
|
|
0805b00c50 | ||
|
|
c8f8bfd6f0 | ||
|
|
1f51d37ae6 | ||
|
|
f8a4b8de51 | ||
|
|
eaa64d7b70 | ||
|
|
4480fbf787 | ||
|
|
c2287033e3 | ||
|
|
0a0fb5287a | ||
|
|
52acbd7d2c | ||
|
|
af4f84a84b | ||
|
|
633241fcee | ||
|
|
f1f3d288cd | ||
|
|
7c98da85a0 | ||
|
|
36f7d64352 | ||
|
|
a4bbcbe5ff | ||
|
|
cbb4ec5aa1 | ||
|
|
84fc5f7d67 | ||
|
|
a4d84cdc21 | ||
|
|
1a422f318c | ||
|
|
8f202fd70d | ||
|
|
dfe96eb30c | ||
|
|
3d7c6c14b3 | ||
|
|
85a4e361d2 | ||
|
|
05b2fa97d3 | ||
|
|
47d57ddf70 | ||
|
|
9521ac6adb | ||
|
|
c2703d215b | ||
|
|
f5c1133fc5 | ||
|
|
f63d81826a | ||
|
|
fa39982a8f | ||
|
|
4b52414e03 | ||
|
|
29b0a7659f | ||
|
|
e69183ce12 | ||
|
|
009c088a64 | ||
|
|
2c1f948832 | ||
|
|
b83e007405 | ||
|
|
1d3ce2c029 | ||
|
|
5ad0730a26 | ||
|
|
52f1383903 | ||
|
|
12b8cbf3e0 | ||
|
|
de44f8565d | ||
|
|
7480ead76a | ||
|
|
e96ae7a650 | ||
|
|
ae524c4d0e | ||
|
|
5adce88c37 | ||
|
|
06dda24431 | ||
|
|
fa046df923 | ||
|
|
2432eb39b1 | ||
|
|
c211a2517f | ||
|
|
73ea0a57a0 | ||
|
|
f05f86dc80 | ||
|
|
e44b25c80f | ||
|
|
bc3acc2826 | ||
|
|
007fb34ca5 | ||
|
|
46a363cce4 | ||
|
|
3047ce57a3 | ||
|
|
7b9ce072d9 | ||
|
|
4d3e3f3aed | ||
|
|
62563ad8a1 | ||
|
|
6fdce63359 | ||
|
|
fdc4d6dda9 | ||
|
|
a3b06ee83f | ||
|
|
43aa62e212 | ||
|
|
70dd3f323e | ||
|
|
803509d952 | ||
|
|
30e85c8654 | ||
|
|
7b24835c9e | ||
|
|
7aee76f5de | ||
|
|
80200a9983 | ||
|
|
79f6b5c181 | ||
|
|
dc02ce3f97 | ||
|
|
845d5a548f | ||
|
|
14a8cf69ef | ||
|
|
c8a6e8005e | ||
|
|
e6bb2bfaae | ||
|
|
8ba2d1cf72 | ||
|
|
a168fc3719 | ||
|
|
fd1dc15576 | ||
|
|
0b82874a52 | ||
|
|
434242858f | ||
|
|
3dfa0525bd | ||
|
|
36d4baaa8e | ||
|
|
7880cba0f9 | ||
|
|
f8d64528b5 | ||
|
|
f6ee61f29e | ||
|
|
2bb1310094 | ||
|
|
f6a137cd43 | ||
|
|
a575fe4934 | ||
|
|
4cd8d8a4a5 | ||
|
|
7a802726fb | ||
|
|
251245d315 | ||
|
|
fbf5a84c4a | ||
|
|
55412b94d2 | ||
|
|
f61fd02ac7 | ||
|
|
dc96b473cd | ||
|
|
abe40c84b0 | ||
|
|
9976fc9723 | ||
|
|
f8092f924a | ||
|
|
075a877284 | ||
|
|
f748395bda | ||
|
|
12018360fd | ||
|
|
821b2fda85 | ||
|
|
4cb2b29187 | ||
|
|
bb59778313 | ||
|
|
ed0ded33b7 | ||
|
|
db19528c24 | ||
|
|
7d1fb0a238 | ||
|
|
11956d9e16 | ||
|
|
cef782c388 | ||
|
|
e87e371c14 | ||
|
|
877a455ae0 | ||
|
|
a47618e986 | ||
|
|
2bbd759c25 | ||
|
|
4dd19884e0 | ||
|
|
5ab0f499ce | ||
|
|
053723647b | ||
|
|
725a44abd8 | ||
|
|
625f2d2410 | ||
|
|
e32748daf2 | ||
|
|
8a2bd1cac3 | ||
|
|
9076f213e6 | ||
|
|
e77edc56fd | ||
|
|
de356304c7 | ||
|
|
8cc9e30f86 | ||
|
|
384323031b | ||
|
|
b40a7b24d4 | ||
|
|
67b2a433a8 | ||
|
|
07bdf02af4 | ||
|
|
35b470dbac | ||
|
|
42eb49b84c | ||
|
|
9d3d11755f | ||
|
|
0fbc546696 | ||
|
|
220422ba0d | ||
|
|
2c9bae8111 | ||
|
|
320f6ddc6e | ||
|
|
0da6d51150 | ||
|
|
65e3170cd9 | ||
|
|
ee3750121c | ||
|
|
9736a4ddd0 | ||
|
|
53e310ff77 | ||
|
|
17285720f1 | ||
|
|
aa690cb9ab | ||
|
|
6d3596087a | ||
|
|
0fe80e8e1f | ||
|
|
f853f20b0c | ||
|
|
39e6fa35e1 | ||
|
|
23b911297e | ||
|
|
6113d1e3eb | ||
|
|
55f3b93958 | ||
|
|
c522987b6f | ||
|
|
8fe64755ec | ||
|
|
cc35328f28 | ||
|
|
a824a7fb73 | ||
|
|
075d7e52df | ||
|
|
cadbae31e4 | ||
|
|
39b5a67040 | ||
|
|
30580b2c57 | ||
|
|
00ab830ad1 | ||
|
|
fcf3577f67 | ||
|
|
2cdfeb8dd0 | ||
|
|
ce300aa75f | ||
|
|
a52caaec75 | ||
|
|
3f3489b292 | ||
|
|
1e059c5649 | ||
|
|
d9e1b2df7f | ||
|
|
94e51a8041 | ||
|
|
81fe90f605 | ||
|
|
69f643447d | ||
|
|
d8355371e3 | ||
|
|
cbe9b9c455 | ||
|
|
dd33c0e582 | ||
|
|
009b0aa361 | ||
|
|
f438176544 | ||
|
|
be8e8a0521 | ||
|
|
1a8b31bdb4 | ||
|
|
8a47055273 | ||
|
|
feb6f7930e | ||
|
|
0271ea7dad | ||
|
|
505b73d20e | ||
|
|
dcad8a9f79 | ||
|
|
9ce58115ab | ||
|
|
1c9d139ff5 | ||
|
|
70794c8eb5 | ||
|
|
c90c4a2e78 | ||
|
|
6797e8af52 | ||
|
|
5109802dfb | ||
|
|
935297b9e8 | ||
|
|
dad209d1cd | ||
|
|
e1072cc8ca | ||
|
|
1a1ba80188 | ||
|
|
c878c91e11 | ||
|
|
c518521d74 | ||
|
|
ce9c9078e5 | ||
|
|
9fe525bca1 | ||
|
|
352865ddaa | ||
|
|
bc950ee40a | ||
|
|
5c23dfd633 | ||
|
|
4633a0450c | ||
|
|
842255766f | ||
|
|
827c32fafd | ||
|
|
f531cd23ee | ||
|
|
2408758360 | ||
|
|
4c792f6f17 | ||
|
|
802ab90d87 | ||
|
|
c268a0ab14 | ||
|
|
b12c7f21b3 | ||
|
|
b3948910ff | ||
|
|
9ee1261204 | ||
|
|
8e3aa0e9ce | ||
|
|
db62a01224 | ||
|
|
d900f2c47c | ||
|
|
82e1fe3f8b | ||
|
|
85eb82e69d | ||
|
|
c529f8099d | ||
|
|
aa3c58917b | ||
|
|
bc591a2399 | ||
|
|
11d996bb56 | ||
|
|
088a78455c | ||
|
|
869d9d487b | ||
|
|
db3978fd4e | ||
|
|
ae7e46fe5e | ||
|
|
4bd85e1804 | ||
|
|
01f4434f6b | ||
|
|
d8f808bf38 | ||
|
|
807dc7d220 | ||
|
|
b14d0c9f60 | ||
|
|
738a1a330c | ||
|
|
676b02c8de | ||
|
|
7136400a33 | ||
|
|
4651d9df68 | ||
|
|
00555a8e9e | ||
|
|
e8c4615ff6 | ||
|
|
763467058b | ||
|
|
c055ba2985 | ||
|
|
7666541905 | ||
|
|
f4b0b39beb | ||
|
|
cfc0925e75 | ||
|
|
4dcc368378 | ||
|
|
8207908d9e | ||
|
|
e08da096dd | ||
|
|
9a903a1ca3 | ||
|
|
1b22bf0e08 | ||
|
|
fc3187a781 | ||
|
|
7fbb4045e2 | ||
|
|
d0dc22794e | ||
|
|
0f235a80e0 | ||
|
|
2a28046382 | ||
|
|
169bbfd2db | ||
|
|
1f52bb35ba | ||
|
|
4a03b3d7d9 | ||
|
|
2544a7e4ea | ||
|
|
26e77ba2c3 | ||
|
|
28c11801f3 | ||
|
|
b3e2ab0f3b | ||
|
|
fb12ba8a2b | ||
|
|
7a5bc864fa | ||
|
|
2e85d4b55a | ||
|
|
80951a8e6e | ||
|
|
9acddede65 | ||
|
|
22165ec1a5 | ||
|
|
d7036aae48 | ||
|
|
fe3924b432 | ||
|
|
510cffb305 | ||
|
|
05c789ae50 | ||
|
|
5d95d61aef | ||
|
|
c61fa71a70 | ||
|
|
3145269f65 | ||
|
|
2d00a1e265 | ||
|
|
f7f178d6e3 | ||
|
|
18c591e0d0 | ||
|
|
749241f4e5 | ||
|
|
997c368604 | ||
|
|
ed28928c84 | ||
|
|
b55c916e77 | ||
|
|
4d537b2a9a | ||
|
|
21f7d6c9b9 | ||
|
|
c1b865d00e | ||
|
|
a32e740242 | ||
|
|
c4f09b5598 | ||
|
|
5a89aa3b32 | ||
|
|
149d57150c | ||
|
|
bbc241748b | ||
|
|
74ecea6307 | ||
|
|
2e829956f4 | ||
|
|
9e2c9cbba9 | ||
|
|
1ad740800b | ||
|
|
c00d4c1a7b | ||
|
|
e0985ebb1c | ||
|
|
d5706442de | ||
|
|
ad5a173f3f | ||
|
|
fbb1d9247f | ||
|
|
63591941b8 | ||
|
|
fdfeec54d7 | ||
|
|
b2404809fc | ||
|
|
f9f463e799 | ||
|
|
f1d3a553d1 | ||
|
|
e997b148e1 | ||
|
|
af906fac03 | ||
|
|
19945df0b3 | ||
|
|
bf83d552f8 | ||
|
|
f5d8e99fc7 | ||
|
|
975037c5a0 | ||
|
|
8b0b45e089 | ||
|
|
182f2ae26e | ||
|
|
26982787ee | ||
|
|
4a42e3ef1b | ||
|
|
4f4498666e | ||
|
|
8f9ba44c2c | ||
|
|
1f91d4fa7b | ||
|
|
cf97d090f3 | ||
|
|
9cd96a65d6 | ||
|
|
5fc75cb4cd | ||
|
|
27fe566412 | ||
|
|
a68c7bf019 | ||
|
|
11388849de | ||
|
|
aa8fb55b30 | ||
|
|
472bea2baa | ||
|
|
0a7a6c64ce | ||
|
|
fde8196874 | ||
|
|
ea8576d344 | ||
|
|
9adda25e00 | ||
|
|
2cc0bf22cb | ||
|
|
892ebc2e03 | ||
|
|
5c9ee03389 | ||
|
|
3702b0c694 | ||
|
|
451d662af4 | ||
|
|
7b8e0a2755 | ||
|
|
63ba4f4f91 | ||
|
|
5f92465d0f | ||
|
|
64c8c5a014 | ||
|
|
2d6322f799 | ||
|
|
8127ce18a3 | ||
|
|
7b5801920b | ||
|
|
c18c1e59df | ||
|
|
4959e664a9 | ||
|
|
fb5ba257ef | ||
|
|
0ed1c0aa6b | ||
|
|
a12fac780b | ||
|
|
0b16e13597 | ||
|
|
45a9e54631 | ||
|
|
b969307c5e | ||
|
|
20325e87be | ||
|
|
fc48d9047a | ||
|
|
16b4db5083 | ||
|
|
4c7487cd2e | ||
|
|
271033e79d | ||
|
|
d2ddcf2d38 | ||
|
|
8a2ab30302 | ||
|
|
3070b0019e | ||
|
|
68ef1fc9e0 | ||
|
|
c13d67dea5 | ||
|
|
b52cf070f5 | ||
|
|
e363cd9813 | ||
|
|
c10fc26cce | ||
|
|
3bb7123dd5 | ||
|
|
2f2a7d1f89 | ||
|
|
013d307bcd | ||
|
|
5e616dd502 | ||
|
|
f200bd2198 | ||
|
|
bd36ee4f67 | ||
|
|
e0e5cc076d | ||
|
|
95e3fb24f3 | ||
|
|
83d47aed2d | ||
|
|
bbd7cf306a | ||
|
|
f6b5c752f4 | ||
|
|
ff20acc367 | ||
|
|
46c2720fc7 | ||
|
|
d3ce8203be | ||
|
|
796e2ec825 | ||
|
|
a597c3f835 | ||
|
|
d2a5edda46 | ||
|
|
3a21efbaae | ||
|
|
e5bcfeaad5 | ||
|
|
af131cd1e5 | ||
|
|
a7bb90e7e6 | ||
|
|
25cf36a948 | ||
|
|
4f7036a405 | ||
|
|
c25d648321 |
20
.github/ISSUE_TEMPLATE/task.yaml
vendored
20
.github/ISSUE_TEMPLATE/task.yaml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 📝 Task
|
||||
description: Create a task for the team to work on, used internally only. We will delete tasks created by non-team members.
|
||||
title: "[Task]: "
|
||||
labels: [Task]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Are you our team member?
|
||||
description: If you are not our team member, please go to discussions.
|
||||
options:
|
||||
- label: Yes, I am?
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: SubTasks
|
||||
placeholder: |
|
||||
- Sub Task 1
|
||||
- Sub Task 2
|
||||
validations:
|
||||
required: false
|
||||
40
.github/workflows/bridge.yml
vendored
40
.github/workflows/bridge.yml
vendored
@@ -1,14 +1,14 @@
|
||||
# This yaml shares the build bridge steps with ci and nightly.
|
||||
name: Build flutter-rust-bridge
|
||||
# 2023-04-19 15:48:00+00:00
|
||||
# 2023-11-23 18:00:00+00:00
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.10.6"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.75.3"
|
||||
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
|
||||
jobs:
|
||||
generate_bridge:
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
@@ -23,21 +23,35 @@ jobs:
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
sudo apt install ca-certificates -y
|
||||
sudo apt update -y
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config
|
||||
sudo apt-get install ca-certificates -y
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
clang \
|
||||
cmake \
|
||||
curl \
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libclang-10-dev \
|
||||
libclang-dev \
|
||||
libgtk-3-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
wget
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: ''
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -74,5 +88,5 @@ jobs:
|
||||
path: |
|
||||
./src/bridge_generated.rs
|
||||
./src/bridge_generated.io.rs
|
||||
./flutter/lib/generated_bridge.dart
|
||||
./flutter/lib/generated_bridge.dart
|
||||
./flutter/lib/generated_bridge.freezed.dart
|
||||
|
||||
67
.github/workflows/ci.yml
vendored
67
.github/workflows/ci.yml
vendored
@@ -1,8 +1,12 @@
|
||||
name: CI
|
||||
|
||||
# env:
|
||||
env:
|
||||
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2023.10.19
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -76,36 +80,63 @@ jobs:
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
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@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
case ${{ matrix.job.target }} in
|
||||
x86_64-unknown-linux-gnu) sudo apt-get -y update ; 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 libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;;
|
||||
x86_64-unknown-linux-gnu)
|
||||
sudo apt-get -y update
|
||||
sudo apt-get install -y \
|
||||
clang \
|
||||
cmake \
|
||||
curl \
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libasound2-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libpulse-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
nasm \
|
||||
wget
|
||||
;;
|
||||
# arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
|
||||
# aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
|
||||
esac
|
||||
|
||||
- name: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: '501db0f17ef6df184fcdbfbe0f87cde2313b6ab1' #2023.04.15
|
||||
|
||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
|
||||
shell: bash
|
||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||
shell: bash
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: ''
|
||||
|
||||
- name: Show version information (Rust, cargo, GCC)
|
||||
shell: bash
|
||||
@@ -117,8 +148,8 @@ jobs:
|
||||
cargo -V
|
||||
rustc -V
|
||||
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -183,7 +214,9 @@ jobs:
|
||||
;;
|
||||
esac;
|
||||
|
||||
echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS}
|
||||
#deprecated echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS}
|
||||
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_ENV
|
||||
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
652
.github/workflows/flutter-build.yml
vendored
652
.github/workflows/flutter-build.yml
vendored
File diff suppressed because it is too large
Load Diff
1
.github/workflows/flutter-ci.yml
vendored
1
.github/workflows/flutter-ci.yml
vendored
@@ -19,4 +19,3 @@ jobs:
|
||||
uses: ./.github/workflows/flutter-build.yml
|
||||
with:
|
||||
upload-artifact: false
|
||||
|
||||
|
||||
2
.github/workflows/flutter-nightly.yml
vendored
2
.github/workflows/flutter-nightly.yml
vendored
@@ -12,4 +12,4 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
upload-artifact: true
|
||||
upload-tag: "nightly"
|
||||
upload-tag: "nightly"
|
||||
|
||||
22
.github/workflows/flutter-tag.yml
vendored
22
.github/workflows/flutter-tag.yml
vendored
@@ -15,4 +15,24 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
upload-artifact: true
|
||||
upload-tag: "1.2.3"
|
||||
upload-tag: ${{ env.GITHUB_REF_NAME }}
|
||||
|
||||
update-fdroid-version-file:
|
||||
name: Publish RustDesk version file for F-Droid updater
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate RustDesk version file
|
||||
run: |
|
||||
UPSTREAM_VERNAME="$GITHUB_REF_NAME"
|
||||
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr -d '.')"
|
||||
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
||||
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
||||
shell: bash
|
||||
|
||||
- name: Publish RustDesk version file
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: "fdroid-version"
|
||||
files: |
|
||||
./rustdesk-version.txt
|
||||
|
||||
28
.github/workflows/history.yml
vendored
28
.github/workflows/history.yml
vendored
@@ -4,13 +4,11 @@ on: [workflow_dispatch]
|
||||
|
||||
env:
|
||||
LLVM_VERSION: "10.0"
|
||||
FLUTTER_VERSION: "3.10.6"
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
TAG_NAME: "tmp"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.75.3"
|
||||
# vcpkg version: 2022.05.10
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
|
||||
VERSION: "1.2.3"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VERSION: "1.2.4"
|
||||
|
||||
jobs:
|
||||
build-for-history-windows:
|
||||
@@ -23,7 +21,7 @@ jobs:
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ matrix.job.ref }}
|
||||
|
||||
@@ -54,14 +52,18 @@ jobs:
|
||||
Push-Location flutter ; flutter pub get ; Pop-Location
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgDirectory: C:\vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
cd C:\
|
||||
git clone https://github.com/Kingtous/rustdesk_thirdpary_lib --depth=1
|
||||
|
||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
env:
|
||||
VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg
|
||||
run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
|
||||
|
||||
- name: Build self-extracted executable
|
||||
@@ -72,7 +74,7 @@ jobs:
|
||||
popd
|
||||
mkdir -p ./SignOutput
|
||||
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ matrix.job.date }}-${{ matrix.job.target }}.exe
|
||||
|
||||
|
||||
- name: Publish Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
|
||||
92
.github/workflows/vcpkg-deps-linux.yml
vendored
92
.github/workflows/vcpkg-deps-linux.yml
vendored
@@ -1,92 +0,0 @@
|
||||
name: Build vcpkg dependencies for linux clients
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build-vcpkg-deps-linux:
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
- { arch: armv7, os: ubuntu-20.04 }
|
||||
- { arch: x86_64, os: ubuntu-20.04 }
|
||||
- { arch: aarch64, os: ubuntu-20.04 }
|
||||
steps:
|
||||
- name: Create vcpkg artifacts folder
|
||||
run: mkdir -p /opt/artifacts
|
||||
|
||||
- name: Cache Vcpkg
|
||||
id: cache-vcpkg
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/artifacts
|
||||
key: vcpkg-${{ matrix.job.arch }}
|
||||
|
||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||
name: Run vcpkg install on ${{ matrix.job.arch }}
|
||||
id: vcpkg
|
||||
with:
|
||||
arch: ${{ matrix.job.arch }}
|
||||
distro: ubuntu18.04
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "/opt/artifacts"
|
||||
dockerRunArgs: |
|
||||
--volume "/opt/artifacts:/artifacts"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt update -y
|
||||
case "${{ matrix.job.arch }}" in
|
||||
x86_64)
|
||||
apt update -y
|
||||
apt install -y curl zip unzip tar git g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev libssl-dev
|
||||
wget https://github.com/Kitware/CMake/releases/download/v3.27.5/cmake-3.27.5.tar.gz
|
||||
apt remove -y --purge cmake
|
||||
tar -zxvf cmake-3.27.5.tar.gz
|
||||
cd cmake-3.27.5
|
||||
./bootstrap
|
||||
make
|
||||
make install
|
||||
cd -
|
||||
cmake --version
|
||||
gcc -v
|
||||
;;
|
||||
aarch64|armv7)
|
||||
apt install -y curl zip unzip git
|
||||
esac
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
case "${{ matrix.job.arch }}" in
|
||||
x86_64)
|
||||
export VCPKG_FORCE_SYSTEM_BINARIES=1
|
||||
pushd /artifacts
|
||||
git clone https://github.com/microsoft/vcpkg.git || true
|
||||
pushd vcpkg
|
||||
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
|
||||
./bootstrap-vcpkg.sh
|
||||
./vcpkg install libvpx libyuv opus aom
|
||||
;;
|
||||
aarch64)
|
||||
pushd /artifacts
|
||||
rm -rf rustdesk_thirdparty_lib
|
||||
git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1
|
||||
mkdir -p /artifacts/vcpkg/installed
|
||||
mv ./rustdesk_thirdparty_lib/vcpkg/installed/arm64-linux /artifacts/vcpkg/installed/arm64-linux
|
||||
;;
|
||||
armv7)
|
||||
pushd /artifacts
|
||||
rm -rf rustdesk_thirdparty_lib
|
||||
git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1
|
||||
mkdir -p /artifacts/vcpkg/installed
|
||||
mv ./rustdesk_thirdparty_lib/vcpkg/installed/arm-linux /artifacts/vcpkg/installed/arm-linux
|
||||
;;
|
||||
esac
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: vcpkg-artifact-${{ matrix.job.arch }}
|
||||
path: |
|
||||
/opt/artifacts/vcpkg/installed
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -49,4 +49,6 @@ lib/generated_bridge.dart
|
||||
.ssh
|
||||
.devcontainer/.*
|
||||
# build cache in examples
|
||||
examples/**/target/
|
||||
examples/**/target/
|
||||
# ===
|
||||
vcpkg_installed
|
||||
3569
Cargo.lock
generated
3569
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
52
Cargo.toml
52
Cargo.toml
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
description = "A remote control software."
|
||||
default-run = "rustdesk"
|
||||
rust-version = "1.75"
|
||||
|
||||
[lib]
|
||||
name = "librustdesk"
|
||||
@@ -27,11 +28,19 @@ use_dasp = ["dasp"]
|
||||
flutter = ["flutter_rust_bridge"]
|
||||
default = ["use_dasp"]
|
||||
hwcodec = ["scrap/hwcodec"]
|
||||
gpucodec = ["scrap/gpucodec"]
|
||||
mediacodec = ["scrap/mediacodec"]
|
||||
linux_headless = ["pam" ]
|
||||
virtual_display_driver = ["virtual_display"]
|
||||
plugin_framework = []
|
||||
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
|
||||
unix-file-copy-paste = [
|
||||
"dep:x11-clipboard",
|
||||
"dep:x11rb",
|
||||
"dep:percent-encoding",
|
||||
"dep:once_cell",
|
||||
"clipboard/unix-file-copy-paste",
|
||||
]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -47,13 +56,12 @@ cfg-if = "1.0"
|
||||
lazy_static = "1.4"
|
||||
sha2 = "0.10"
|
||||
repng = "0.2"
|
||||
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
|
||||
parity-tokio-ipc = { git = "https://github.com/rustdesk-org/parity-tokio-ipc" }
|
||||
runas = "=1.0" # https://github.com/mitsuhiko/rust-runas/issues/13
|
||||
magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" }
|
||||
magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" }
|
||||
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
|
||||
rubato = { version = "0.12", optional = true }
|
||||
samplerate = { version = "0.2", optional = true }
|
||||
async-trait = "0.1"
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
clap = "4.2"
|
||||
rpassword = "7.2"
|
||||
@@ -62,18 +70,19 @@ num_cpus = "1.15"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
default-net = "0.14"
|
||||
wol-rs = "1.0"
|
||||
flutter_rust_bridge = { version = "1.75", features = ["uuid"], optional = true}
|
||||
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
||||
errno = "0.3"
|
||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
||||
url = { version = "2.3", features = ["serde"] }
|
||||
crossbeam-queue = "0.3"
|
||||
hex = "0.4"
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls"], default-features=false }
|
||||
chrono = "0.4"
|
||||
cidr-utils = "0.5"
|
||||
libloading = "0.8"
|
||||
fon = "0.6"
|
||||
zip = "0.6"
|
||||
shutdown_hooks = "0.1"
|
||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
|
||||
cpal = "0.15"
|
||||
@@ -86,17 +95,17 @@ sys-locale = "0.3"
|
||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||
clipboard = { path = "libs/clipboard" }
|
||||
ctrlc = "3.2"
|
||||
arboard = "3.2"
|
||||
arboard = { version = "3.2", features = ["wayland-data-control"] }
|
||||
system_shutdown = "4.0"
|
||||
qrcode-generator = "4.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi"] }
|
||||
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] }
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
virtual_display = { path = "libs/virtual_display", optional = true }
|
||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
||||
shared_memory = "0.12"
|
||||
shutdown_hooks = "0.1"
|
||||
tauri-winrt-notification = "0.1.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
@@ -106,18 +115,27 @@ dispatch = "0.2"
|
||||
core-foundation = "0.9"
|
||||
core-graphics = "0.22"
|
||||
include_dir = "0.7"
|
||||
dark-light = "1.0"
|
||||
fruitbasket = "0.10"
|
||||
objc_id = "0.1"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
tray-icon = { git = "https://github.com/rustdesk-org/tray-icon" }
|
||||
tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
|
||||
tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" }
|
||||
image = "0.24"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
|
||||
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
wallpaper = { git = "https://github.com/21pages/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", "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", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||
@@ -129,10 +147,15 @@ dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
pam = { git="https://github.com/fufesou/pam", optional = true }
|
||||
users = { version = "0.11" }
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
percent-encoding = {version = "2.3", optional = true}
|
||||
once_cell = {version = "1.18", optional = true}
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
jni = "0.21"
|
||||
android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
|
||||
|
||||
[workspace]
|
||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
||||
@@ -145,12 +168,11 @@ FileDescription = "RustDesk"
|
||||
|
||||
[target.'cfg(target_os="windows")'.build-dependencies]
|
||||
winres = "0.1"
|
||||
winapi = { version = "0.3", features = [ "winnt" ] }
|
||||
winapi = { version = "0.3", features = [ "winnt", "pdh", "synchapi" ] }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
flutter_rust_bridge_codegen = "1.75"
|
||||
os-version = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -170,3 +192,7 @@ panic = 'abort'
|
||||
strip = true
|
||||
#opt-level = 'z' # only have smaller size after strip
|
||||
rpath = true
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = '...' # Platform-specific.
|
||||
#strip = "debuginfo"
|
||||
|
||||
55
Dockerfile
55
Dockerfile
@@ -1,21 +1,54 @@
|
||||
FROM debian
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
WORKDIR /
|
||||
RUN apt update -y && apt install -y g++ gcc git curl nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev cmake ninja-build && rm -rf /var/lib/apt/lists/*
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt update -y && \
|
||||
apt install --yes --no-install-recommends \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
curl \
|
||||
nasm \
|
||||
yasm \
|
||||
libgtk-3-dev \
|
||||
clang \
|
||||
libxcb-randr0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
libasound2-dev \
|
||||
libpulse-dev \
|
||||
make \
|
||||
cmake \
|
||||
unzip \
|
||||
zip \
|
||||
sudo \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
ca-certificates \
|
||||
ninja-build && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg
|
||||
RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics
|
||||
RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom
|
||||
RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \
|
||||
/vcpkg/bootstrap-vcpkg.sh -disableMetrics && \
|
||||
/vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom
|
||||
|
||||
RUN groupadd -r user && \
|
||||
useradd -r -g user user --home /home/user && \
|
||||
mkdir -p /home/user/rustdesk && \
|
||||
chown -R user: /home/user && \
|
||||
echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user
|
||||
|
||||
RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user
|
||||
WORKDIR /home/user
|
||||
RUN curl -LO https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
|
||||
USER user
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh
|
||||
RUN chmod +x rustup.sh
|
||||
RUN ./rustup.sh -y
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh && \
|
||||
chmod +x rustup.sh && \
|
||||
./rustup.sh -y
|
||||
|
||||
USER root
|
||||
ENV HOME=/home/user
|
||||
COPY ./entrypoint /
|
||||
ENTRYPOINT ["/entrypoint"]
|
||||
COPY ./entrypoint.sh /
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
40
README.md
40
README.md
@@ -11,7 +11,7 @@
|
||||
|
||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
@@ -49,7 +49,7 @@ Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Desktop versions use [Sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only.
|
||||
Desktop versions use Flutter or Sciter (deprecated) for GUI, this tutorial is for Sciter only, since it is easier and more friendly to start. Check out our [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for building Flutter version.
|
||||
|
||||
Please download Sciter dynamic library yourself.
|
||||
|
||||
@@ -80,11 +80,12 @@ sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxc
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
### 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
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
@@ -135,34 +136,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Change Wayland to X11 (Xorg)
|
||||
|
||||
RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) to configuring Xorg as the default GNOME session.
|
||||
|
||||
## Wayland support
|
||||
|
||||
Wayland does not seem to provide any API for sending keypresses to other windows. Therefore, the RustDesk uses an API from a lower level, namely the `/dev/uinput` device (Linux kernel level).
|
||||
|
||||
When Wayland is the controlled side, you have to start in the following way:
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Notice**: Wayland screen recording uses different interfaces. RustDesk currently only supports org.freedesktop.portal.ScreenCast.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## How to build with Docker
|
||||
|
||||
Begin by cloning the repository and building the Docker container:
|
||||
@@ -198,12 +171,13 @@ Please ensure that you are running these commands from the root of the RustDesk
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: file copy and paste implementation for Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: obsolete Sciter UI (deprecated)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
|
||||
|
||||
## Snapshots
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
version: 1
|
||||
script:
|
||||
- rm -rf ./AppDir || true
|
||||
- bsdtar -zxvf ../rustdesk-1.2.3.deb
|
||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
||||
- tar -xvf ./data.tar.xz
|
||||
- mkdir ./AppDir
|
||||
- mv ./usr ./AppDir/usr
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.2.3
|
||||
version: 1.2.4
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
version: 1
|
||||
script:
|
||||
- rm -rf ./AppDir || true
|
||||
- bsdtar -zxvf ../rustdesk-1.2.3.deb
|
||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
||||
- tar -xvf ./data.tar.xz
|
||||
- mkdir ./AppDir
|
||||
- mv ./usr ./AppDir/usr
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.2.3
|
||||
version: 1.2.4
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
53
build.py
53
build.py
@@ -16,7 +16,7 @@ osx = platform.platform().startswith(
|
||||
hbb_name = 'rustdesk' + ('.exe' if windows else '')
|
||||
exe_path = 'target/release/' + hbb_name
|
||||
if windows:
|
||||
flutter_build_dir = 'build/windows/runner/Release/'
|
||||
flutter_build_dir = 'build/windows/x64/runner/Release/'
|
||||
elif osx:
|
||||
flutter_build_dir = 'build/macos/Build/Products/Release/'
|
||||
else:
|
||||
@@ -24,18 +24,21 @@ else:
|
||||
flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
|
||||
skip_cargo = False
|
||||
|
||||
|
||||
def get_arch() -> str:
|
||||
custom_arch = os.environ.get("ARCH")
|
||||
if custom_arch is None:
|
||||
return "amd64"
|
||||
return custom_arch
|
||||
|
||||
|
||||
def system2(cmd):
|
||||
err = os.system(cmd)
|
||||
if err != 0:
|
||||
print(f"Error occurred when executing: {cmd}. Exiting.")
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def get_version():
|
||||
with open("Cargo.toml", encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
@@ -46,17 +49,11 @@ def get_version():
|
||||
|
||||
def parse_rc_features(feature):
|
||||
available_features = {
|
||||
'IddDriver': {
|
||||
'platform': ['windows'],
|
||||
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.3/RustDeskIddDriver_x64.zip',
|
||||
'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.3/checksum_md5',
|
||||
'exclude': ['README.md', 'certmgr.exe', 'install_cert_runas_admin.bat', 'RustDeskIddApp.exe'],
|
||||
},
|
||||
'PrivacyMode': {
|
||||
'platform': ['windows'],
|
||||
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1'
|
||||
'/TempTopMostWindow_x64_pic_en.zip',
|
||||
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5',
|
||||
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3'
|
||||
'/TempTopMostWindow_x64.zip',
|
||||
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3/checksum_md5',
|
||||
'include': ['WindowInjection.dll'],
|
||||
}
|
||||
}
|
||||
@@ -109,7 +106,7 @@ def make_parser():
|
||||
nargs='+',
|
||||
default='',
|
||||
help='Integrate features, windows only.'
|
||||
'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
||||
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
||||
parser.add_argument('--flutter', action='store_true',
|
||||
help='Build flutter package', default=False)
|
||||
parser.add_argument(
|
||||
@@ -118,11 +115,21 @@ def make_parser():
|
||||
help='Enable feature hwcodec' + (
|
||||
'' if windows or osx else ', need libva-dev, libvdpau-dev.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--gpucodec',
|
||||
action='store_true',
|
||||
help='Enable feature gpucodec, only available on windows now.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--portable',
|
||||
action='store_true',
|
||||
help='Build windows portable'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--unix-file-copy-paste',
|
||||
action='store_true',
|
||||
help='Build with unix file copy paste feature'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--flatpak',
|
||||
action='store_true',
|
||||
@@ -172,8 +179,8 @@ def generate_build_script_for_docker():
|
||||
export VCPKG_ROOT=`pwd`/vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
popd
|
||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||
# build rustdesk
|
||||
./build.py --flutter --hwcodec
|
||||
''')
|
||||
@@ -185,6 +192,7 @@ def download_extract_features(features, res_dir):
|
||||
import re
|
||||
|
||||
proxy = ''
|
||||
|
||||
def req(url):
|
||||
if not proxy:
|
||||
return url
|
||||
@@ -196,9 +204,9 @@ def download_extract_features(features, res_dir):
|
||||
|
||||
for (feat, feat_info) in features.items():
|
||||
includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else []
|
||||
includes = [ re.compile(p) for p in includes ]
|
||||
includes = [re.compile(p) for p in includes]
|
||||
excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else []
|
||||
excludes = [ re.compile(p) for p in excludes ]
|
||||
excludes = [re.compile(p) for p in excludes]
|
||||
|
||||
print(f'{feat} download begin')
|
||||
download_filename = feat_info['zip_url'].split('/')[-1]
|
||||
@@ -261,10 +269,10 @@ def external_resources(flutter, args, res_dir):
|
||||
|
||||
def get_features(args):
|
||||
features = ['inline'] if not args.flutter else []
|
||||
if windows:
|
||||
features.append('virtual_display_driver')
|
||||
if args.hwcodec:
|
||||
features.append('hwcodec')
|
||||
if args.gpucodec:
|
||||
features.append('gpucodec')
|
||||
if args.flutter:
|
||||
features.append('flutter')
|
||||
features.append('flutter_texture_render')
|
||||
@@ -272,6 +280,8 @@ def get_features(args):
|
||||
features.append('flatpak')
|
||||
if args.appimage:
|
||||
features.append('appimage')
|
||||
if args.unix_file_copy_paste:
|
||||
features.append('unix-file-copy-paste')
|
||||
print("features:", features)
|
||||
return features
|
||||
|
||||
@@ -350,6 +360,7 @@ def build_flutter_deb(version, features):
|
||||
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
|
||||
os.chdir("..")
|
||||
|
||||
|
||||
def build_deb_from_folder(version, binary_folder):
|
||||
os.chdir('flutter')
|
||||
system2('mkdir -p tmpdeb/usr/bin/')
|
||||
@@ -388,10 +399,12 @@ def build_deb_from_folder(version, binary_folder):
|
||||
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
|
||||
os.chdir("..")
|
||||
|
||||
|
||||
def build_flutter_dmg(version, features):
|
||||
if not skip_cargo:
|
||||
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
|
||||
system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
|
||||
system2(
|
||||
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
|
||||
# copy dylib
|
||||
system2(
|
||||
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
||||
@@ -481,6 +494,7 @@ def main():
|
||||
system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
|
||||
pa = os.environ.get('P')
|
||||
if pa:
|
||||
# https://certera.com/kb/tutorial-guide-for-safenet-authentication-client-for-code-signing/
|
||||
system2(
|
||||
f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com '
|
||||
'target\\release\\rustdesk.exe')
|
||||
@@ -557,7 +571,8 @@ def main():
|
||||
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/*
|
||||
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
|
||||
'''.format(pa))
|
||||
system2('create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version)
|
||||
system2(
|
||||
'create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version)
|
||||
os.rename('RustDesk %s.dmg' %
|
||||
version, 'rustdesk-%s.dmg' % version)
|
||||
if pa:
|
||||
@@ -577,7 +592,7 @@ def main():
|
||||
else:
|
||||
print('Not signed')
|
||||
else:
|
||||
# buid deb package
|
||||
# build deb package
|
||||
system2(
|
||||
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
|
||||
system2('dpkg-deb -R rustdesk.deb tmpdeb')
|
||||
|
||||
56
build.rs
56
build.rs
@@ -41,7 +41,7 @@ fn build_manifest() {
|
||||
}
|
||||
}
|
||||
|
||||
fn install_oboe() {
|
||||
fn install_android_deps() {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
if target_os != "android" {
|
||||
return;
|
||||
@@ -49,6 +49,8 @@ fn install_oboe() {
|
||||
let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||
if target_arch == "x86_64" {
|
||||
target_arch = "x64".to_owned();
|
||||
} else if target_arch == "x86" {
|
||||
target_arch = "x86".to_owned();
|
||||
} else if target_arch == "aarch64" {
|
||||
target_arch = "arm64".to_owned();
|
||||
} else {
|
||||
@@ -66,62 +68,16 @@ fn install_oboe() {
|
||||
path.join("lib").to_str().unwrap()
|
||||
)
|
||||
);
|
||||
println!("cargo:rustc-link-lib=ndk_compat");
|
||||
println!("cargo:rustc-link-lib=oboe");
|
||||
println!("cargo:rustc-link-lib=oboe_wrapper");
|
||||
println!("cargo:rustc-link-lib=c++");
|
||||
println!("cargo:rustc-link-lib=OpenSLES");
|
||||
// I always got some strange link error with oboe, so as workaround, put oboe.cc into oboe src: src/common/AudioStreamBuilder.cpp
|
||||
// also to avoid libc++_shared not found issue, cp ndk's libc++_shared.so to jniLibs, e.g.
|
||||
// ./flutter_hbb/android/app/src/main/jniLibs/arm64-v8a/libc++_shared.so
|
||||
// let include = path.join("include");
|
||||
//cc::Build::new().file("oboe.cc").include(include).compile("oboe_wrapper");
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
fn gen_flutter_rust_bridge() {
|
||||
if !std::env::var("RUN_FFIGEN").is_ok() {
|
||||
return;
|
||||
}
|
||||
use lib_flutter_rust_bridge_codegen::{
|
||||
config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts,
|
||||
};
|
||||
let llvm_path = match std::env::var("LLVM_HOME") {
|
||||
Ok(path) => Some(vec![path]),
|
||||
Err(_) => None,
|
||||
};
|
||||
// Tell Cargo that if the given file changes, to rerun this build script.
|
||||
println!("cargo:rerun-if-changed=src/flutter_ffi.rs");
|
||||
// Options for frb_codegen
|
||||
let raw_opts = RawOpts {
|
||||
// Path of input Rust code
|
||||
rust_input: vec!["src/flutter_ffi.rs".to_string()],
|
||||
// Path of output generated Dart code
|
||||
dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()],
|
||||
// Path of output generated C header
|
||||
c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]),
|
||||
/// Path to the installed LLVM
|
||||
llvm_path,
|
||||
// for other options use defaults
|
||||
..Default::default()
|
||||
};
|
||||
// get opts from raw opts
|
||||
let configs = config_parse(raw_opts);
|
||||
// generation of rust api for ffi
|
||||
let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap();
|
||||
for config in configs.iter() {
|
||||
frb_codegen(config, &all_symbols).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
hbb_common::gen_version();
|
||||
install_oboe();
|
||||
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
||||
// let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
// if target_os == "android" || target_os == "ios" {
|
||||
#[cfg(feature = "flutter")]
|
||||
gen_flutter_rust_bridge();
|
||||
// return;
|
||||
// }
|
||||
install_android_deps();
|
||||
#[cfg(all(windows, feature = "inline"))]
|
||||
build_manifest();
|
||||
#[cfg(windows)]
|
||||
|
||||
101
docs/CODE_OF_CONDUCT-JP.md
Normal file
101
docs/CODE_OF_CONDUCT-JP.md
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
# コントリビューター規約 行動規範
|
||||
|
||||
## 私たちの誓い
|
||||
|
||||
私たちは、メンバー、貢献者、リーダーとして、年齢、体格、目に見える・見えない障害、
|
||||
民族性、性の特徴、性自認と表現、経験のレベル、教育、社会経済的地位、国籍、個人の外見、
|
||||
人種、宗教、性的自認と指向に関係なく、誰もがハラスメントのないコミュニティに参加できるようにすることを誓います。
|
||||
|
||||
私たちは、開かれた、歓迎された、多様で、包容力のある、健全な地域社会に貢献するように行動し、交流することを誓います。
|
||||
|
||||
## 私たちの基準
|
||||
|
||||
地域社会にとって好ましい環境にコントリビュートする行動の例には、以下のようなものがある:
|
||||
|
||||
* 他者への共感と優しさ
|
||||
* 異なる意見、視点、経験を尊重すること
|
||||
* 建設的なフィードバックを与え、潔く受け入れること
|
||||
* 私たちの過ちによって影響を受けた人々に責任を受け入れ、謝罪し、経験から学ぶこと
|
||||
* 私たち個人にとってだけでなく、地域社会全体にとって何が最善であるかに焦点を合わせること
|
||||
|
||||
許されない行為の例:
|
||||
|
||||
* 性的な言葉やイメージの使用、性的な注目や誘いかけ
|
||||
* 荒らし、侮辱的または軽蔑的なコメント、個人的または政治的な攻撃
|
||||
* 公的または私的な嫌がらせ
|
||||
* 明示的な許可なく、他人の住所や電子メールアドレスなどの個人情報を公開すること
|
||||
* 職業上不適切と見なされるその他の行為
|
||||
|
||||
## 執行責任
|
||||
|
||||
コミュニティリーダーは、許容される行動の基準を明確にし、実施する責任があり、
|
||||
不適切、脅迫的、攻撃的、または有害と判断される行動に対しては、適切かつ公正な是正措置をとります
|
||||
|
||||
コミュニティリーダーは、本行動規範に沿わないコメント、コミット、コード、ウィキ編集、
|
||||
課題、その他の貢献を削除、編集、拒否する権利と責任を有し、適切な場合にはモデレーション決定の理由を伝えます。
|
||||
|
||||
## スコープ
|
||||
|
||||
この行動規範は、すべてのコミュニティスペースで適用され、また個人が公的なスペースでコミュニティを公式に代表している場合にも適用されます。
|
||||
当コミュニティを代表する例としては、公式 E メールアドレスの使用、公式ソーシャルメディアアカウントによる投稿、
|
||||
オンラインまたはオフラインのイベントでの任命された代表としての行動などが挙げられます。
|
||||
|
||||
## 施行
|
||||
|
||||
虐待、ハラスメント、その他容認できない行為があった場合は、[info@rustdesk.com](mailto:info@rustdesk.com) の
|
||||
執行担当コミュニティリーダーに報告することができる。
|
||||
すべての苦情は、迅速かつ公正に検討・調査されます。
|
||||
|
||||
すべての地域社会の指導者は、いかなる事件の報告者のプライバシーと安全を尊重する義務がある。
|
||||
|
||||
## 執行ガイドライン
|
||||
|
||||
コミュニティリーダーは、本行動規範に違反すると判断した行為に対する結果を決定する際、
|
||||
以下の「コミュニティへの影響に関するガイドライン」に従います:
|
||||
|
||||
### 1. 修正
|
||||
|
||||
**コミュニティへの影響**: 不適切な言葉の使用、またはプロフェッショナルでない、あるいは地域社会で歓迎されないとみなされるその他の行動。
|
||||
|
||||
**結果**: コミュニティリーダーからの私的な書面による警告。違反の性質と、
|
||||
なぜその行為が不適切であったのかについての説明を明確にする。公的な謝罪が要求される場合もある。
|
||||
|
||||
### 2. 警告
|
||||
|
||||
**コミュニティへの影響**: 単一の出来事または一連の行動による違反。
|
||||
|
||||
**結果**: 行動を続けた場合の結果を伴う警告。一定期間、行動規範の実施者との勝手な交流を含め、
|
||||
関係者と交流しないこと。これには、ソーシャルメディアなどの外部チャンネルだけでなく、
|
||||
コミュニティスペースでの交流を避けることも含まれます。これらの条件に違反した場合、一時的または恒久的に追放される可能性があります。
|
||||
|
||||
### 3. 一時的な禁止
|
||||
|
||||
**コミュニティへの影響**: 継続的な不適切な行動を含む、コミュニティ基準に対する重大な違反。
|
||||
|
||||
**結果**: 一定期間、地域社会とのあらゆる交流や公的なコミュニケーションを一時的に禁止すること。
|
||||
この期間中は、行動規範を執行する人々との未承諾の交流を含め、関係者との公私にわたる交流は許されない。
|
||||
これらの条件に違反した場合、永久禁止となる可能性があります。
|
||||
|
||||
### 4. 永久禁止
|
||||
|
||||
**コミュニティへの影響**: 継続的な不適切な行動、個人に対する嫌がらせ、
|
||||
または個人クラスに対する攻撃や中傷など、地域社会の基準に対する違反のパターンを示すこと。
|
||||
|
||||
**結果**: コミュニティ内でのあらゆる公的交流の永久禁止。
|
||||
|
||||
## 帰属
|
||||
|
||||
この行動規範は、[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0] に掲載されている
|
||||
[コントリビューター規約][ホームページ]、バージョン 2.0 から引用したものです。
|
||||
|
||||
コミュニティインパクトガイドラインは、[Mozilla's code of conduct enforcement ladder][Mozilla CoC] に触発されました。
|
||||
|
||||
この行動規範に関するよくある質問については、[https://www.contributor-covenant.org/faq][FAQ] の FAQ をご覧ください。
|
||||
翻訳は [https://www.contributor-covenant.org/translations][翻訳] にあります。
|
||||
|
||||
[ホームページ]: 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
|
||||
[翻訳]: https://www.contributor-covenant.org/translations
|
||||
41
docs/CONTRIBUTING-JP.md
Normal file
41
docs/CONTRIBUTING-JP.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# RustDesk へのコントリビュート
|
||||
|
||||
RustDesk は皆さんからのコントリビュートを歓迎します。ご協力いただける方のガイドラインは
|
||||
以下の通りです:
|
||||
|
||||
## コントリビューション
|
||||
|
||||
RustDesk またはその依存関係へのコントリビュートは、GitHub のプルリクエストの形で行ってください。
|
||||
それぞれのプルリクエストは、コアコントリビューター(パッチの適用を許可されている人)によってレビューされ、
|
||||
メインツリーに適用されるか、必要な変更についてのフィードバックが与えられます。
|
||||
コアコントリビューターからのものであっても、すべてのコントリビューターはこのフォーマットに従うべきです。
|
||||
|
||||
ある issue に取り組みたい場合は、GitHub の issue にコメントすることで、まずその対応を主張してください。
|
||||
これは、同じ issue に対するコントリビューターの重複作業を防ぐためです。
|
||||
|
||||
## プルリクエストのチェックリスト
|
||||
|
||||
- master ブランチからブランチし、必要であればプルリクエストを提出する前に現在の master ブランチにリベースしてください。
|
||||
master と正しくマージできない場合、変更をリベースするよう求められる可能性があります。
|
||||
|
||||
- コミットは、各コミットが独立して正しい(すなわち、各コミットがコンパイルされ、テストに合格する)ことを保証しながら、
|
||||
可能な限り小さくすべきです。
|
||||
|
||||
- コミットには、Developer Certificate of Origin (http://developercertificate.org) の sign-off を添えてください。
|
||||
これは、あなた(および該当する場合はあなたの雇用主)が [プロジェクトのライセンス](../LICENCE) の条項に拘束されることに
|
||||
同意していることを示すものです。git では、これは `git commit` の `-s` オプションを使います。
|
||||
|
||||
- もしあなたのパッチがレビューされなかったり、特定の人にレビューしてもらう必要がある場合、
|
||||
プルリクエストやコメントでレビューを依頼するレビュアーに@返信したり、[email](mailto:info@rustdesk.com) でレビューを依頼することができます。
|
||||
|
||||
- 修正したバグや新機能に関連するテストを追加する。
|
||||
|
||||
具体的なgitの手順については、[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)を参照してください。
|
||||
|
||||
## 行動規範
|
||||
|
||||
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
|
||||
|
||||
## コミュニケーション
|
||||
|
||||
RustDesk のコントリビューターは、[Discord](https://discord.gg/nDceKgxnkV) を良く使っています。
|
||||
14
docs/DEVCONTAINER-JP.md
Normal file
14
docs/DEVCONTAINER-JP.md
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -118,10 +118,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### X11 (Xorg) إلى Wayland تغيير
|
||||
|
||||
افتراضية GNOME session ك Xorg إتبع [هذه](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) الخطوات لإعداد Wayland لا تدعم RustDesk
|
||||
|
||||
## Docker طريقة البناء باستخدام
|
||||
|
||||
ابدأ باستنساخ المستودع وبناء الكونتاينر:
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<a href="#file-structure">Struktura</a> •
|
||||
<a href="#snapshot">Ukázky</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</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-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, <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>
|
||||
<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>
|
||||
|
||||
Dopisujte 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)
|
||||
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
@@ -44,7 +44,7 @@ Varianta pro mobilní platformy používá aplikační rámec (framework) Flutte
|
||||
|
||||
- Připravte si vývojové prostředí pro jazyky Rust a C++
|
||||
|
||||
- Nainstalujte [vcpkg](https://github.com/microsoft/vcpkg), a nastavte správně proměnnou prostsředí `VCPKG_ROOT`
|
||||
- Nainstalujte [vcpkg](https://github.com/microsoft/vcpkg), a správně nastavte proměnnou prostředí `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
|
||||
@@ -111,10 +111,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Změna z Wayland na X11 (Xorg)
|
||||
|
||||
RustDesk (zatím) nepodporuje zobrazovací server Wayland. Jak nastavit Xorg jako výchozí pro relace v prostředí GNOME naleznete [zde](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/).
|
||||
|
||||
## Jak sestavit prostřednictvím Docker kontejnerizace
|
||||
|
||||
Začněte tím, že si naklonujete tento repozitář a sestavíte docker kontejner:
|
||||
@@ -131,7 +127,7 @@ Poté pokaždé, když bude třeba aplikaci sestavit, spusťte následující p
|
||||
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
|
||||
```
|
||||
|
||||
Všimněte si, že prvotní sestavení může trvat déle (než se do mezipaměti uloží veškeré softwarové součásti, které jsou potřeba) – následná opakování už budou rychlejší. Dále, pokud potřebujete příkazu pro sestavení zadat nějaké argumenty, je možné je zapsat na konec příkazu na pozici `<OPTIONAL-ARGS>`. Například, pokud byste chtěli sestavit optimalizovaně pro vydání, spustili byste výše uvedený příkaz následovaný `--release`. Výsledný spustitelný soubor se objeví v cílové složce na vašem systému a bude ho možné spustit pomocí:
|
||||
Všimněte si, že prvotní sestavení může trvat déle (než se do mezipaměti uloží veškeré softwarové součásti, které jsou potřeba) – následná opakování už budou rychlejší. Pokud navíc potřebujete zadat různé argumenty příkazu pro sestavení, můžete tak učinit na konci příkazu v pozici `<OPTIONAL-ARGS>`. Například, pokud byste chtěli sestavit optimalizovanou verzi pro vydání, spustili byste výše uvedený příkaz následovaný `--release`. Výsledný spustitelný soubor se objeví v cílové složce na vašem systému a bude ho možné spustit pomocí:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
@@ -143,7 +139,7 @@ Nebo, pokud spouštíte variantu pro vydání:
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Zajistětě, abyste tyto příkazy spouštěli z kořene repozitáře s RustDesk, jinak aplikace nemusí být schopná nalézt potřebné prostředky (resources). Také si všimněte, že ostatní dílčí príkazy nástroje cargo, jako třeba `install` nebo `run` zatím nejsou prostřednictvím této metody podporovány, protože by vedly k instalaci či spuštění program uvnitř kontejneru namísto přímo v systému.
|
||||
Ujistěte se, že tyto příkazy spouštíte z kořenového adresáře RustDesk, jinak aplikace nemusí být schopná nalézt potřebné prostředky (resources). Také si všimněte, že ostatní dílčí príkazy nástroje cargo, jako třeba `install` nebo `run` zatím nejsou prostřednictvím této metody podporovány, protože by vedly k instalaci či spuštění program uvnitř kontejneru namísto přímo v systému.
|
||||
|
||||
## Struktura souborů
|
||||
|
||||
|
||||
@@ -108,33 +108,6 @@ mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Skift Wayland til X11 (Xorg)
|
||||
|
||||
RustDesk understøtter ikke Wayland. Tjek [dette](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) for at konfigurere Xorg som standard GNOME-session.
|
||||
|
||||
## Wayland-support
|
||||
|
||||
Wayland ser ikke ud til at levere nogen API til at sende tastetryk til andre vinduer. Derfor bruger rustdesk et API fra et lavere niveau, nemlig `/dev/uinput`-enheden (Linux-kerneniveau).
|
||||
|
||||
Når wayland er den kontrollerede side, skal du starte på følgende måde:
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Bemærk**: Wayland-skærmoptagelse bruger forskellige grænseflader. RustDesk understøtter i øjeblikket kun org.freedesktop.portal.ScreenCast.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
## Sådan bygger du med Docker
|
||||
|
||||
```sh
|
||||
|
||||
@@ -133,34 +133,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Wayland zu X11 (Xorg) ändern
|
||||
|
||||
RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen.
|
||||
|
||||
## Wayland-Unterstützung
|
||||
|
||||
Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene).
|
||||
|
||||
Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen:
|
||||
```bash
|
||||
# Dienst uinput starten
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur org.freedesktop.portal.ScreenCast.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Keine Unterstützung
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Unterstützung
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Auf Docker kompilieren
|
||||
|
||||
Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen:
|
||||
|
||||
@@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Ŝanĝi Wayland por X11 (Xorg)
|
||||
|
||||
RustDesk ne subtenas Wayland. Kontrolu [tion](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) por agordi Xorg kiel defaŭlta sesio GNOME.
|
||||
|
||||
## Kiel kompili kun Docker
|
||||
|
||||
Komencu klonante la deponejon kaj kompilu la konteneron Docker:
|
||||
|
||||
@@ -113,34 +113,6 @@ mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Cambia Wayland a X11 (Xorg)
|
||||
|
||||
RustDesk no soporta Wayland. Lee [esto](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME.
|
||||
|
||||
## Soporte para Wayland
|
||||
|
||||
Wayland no parece proporcionar ninguna API para enviar pulsaciones de teclas a otras ventanas. Por lo tanto, rustdesk usa una API de nivel bajo, a saber, el dispositivo `/dev/uinput` (a nivel del kernel de Linux).
|
||||
|
||||
Cuando wayland esta del lado controlado, hay que iniciar de la siguiente manera:
|
||||
```bash
|
||||
# Empezar el servicio uinput
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Aviso**: La grabación de pantalla de Wayland utiliza diferentes interfaces. RustDesk actualmente sólo soporta org.freedesktop.portal.ScreenCast
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# No soportado
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Soportado
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Como compilar con Docker
|
||||
|
||||
Empieza clonando el repositorio y compilando el contenedor de docker:
|
||||
|
||||
@@ -112,10 +112,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### تغییر Wayland به (X11 (Xorg
|
||||
|
||||
راستدسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیشفرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید.
|
||||
|
||||
## نحوه ساخت با داکر
|
||||
|
||||
این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید
|
||||
|
||||
@@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Vaihda Wayland-ympäristö X11 (Xorg)-ympäristöön
|
||||
|
||||
RustDesk ei tue Waylandia. Tarkista [tämä](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) asettamalla Xorg oletus GNOME-istuntoon.
|
||||
|
||||
## Kuinka rakennetaan Dockerin kanssa
|
||||
|
||||
Aloita kloonaamalla tietovarasto ja rakentamalla docker-säiliö:
|
||||
|
||||
@@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug
|
||||
Exécution du cargo
|
||||
```
|
||||
|
||||
### Changer Wayland en X11 (Xorg)
|
||||
|
||||
RustDesk ne supporte pas Wayland. Lisez [cela](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) pour configurer Xorg comme la session GNOME par défaut.
|
||||
|
||||
## Comment construire avec Docker
|
||||
|
||||
Commencez par cloner le dépôt et construire le conteneur Docker :
|
||||
|
||||
@@ -133,34 +133,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Αλλαγή του Wayland σε X11 (Xorg)
|
||||
|
||||
Το RustDesk δεν υποστηρίζει το πρωτόκολλο Wayland. Διαβάστε [εδώ](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) ώστε να ορίσετε το Xorg ως το προκαθορισμένο GNOME περιβάλλον.
|
||||
|
||||
## Υποστήριξη Wayland
|
||||
|
||||
Το Wayland προς το παρόν δεν διαθέτει κάποιο API το οποίο να στέλνει τα πατήματα πλήκτρων στα υπόλοιπα παράθυρα. Για τον λόγο αυτό, το Rustdesk χρησιμοποιεί ένα API από κατώτερο επίπεδο, όπως το `/dev/uinput` (Linux kernel level).
|
||||
|
||||
Σε περίπτωση που το Wayland είναι η ελεγχόμενη πλευρά, θα πρέπει να ξεκινήσετε με τον παρακάτω τρόπο:
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Σημείωση**: Η εγγραφή οθόνης του Wayland χρησιμοποιεί διαφορετικές διεπαφές. Το RustDesk προς το παρόν υποστηρίζει μόνο org.freedesktop.portal.ScreenCast.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Πως να κάνετε build στο Docker
|
||||
|
||||
Ξεκινήστε κλωνοποιώντας το αποθετήριο και κάνοντας build το docker container:
|
||||
@@ -189,7 +161,7 @@ target/debug/rustdesk
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Βεβαιωθείτε ότι εκτελείτε αυτές τις εντολές από την αρχική διαδρομή του αποθετηρίου του Rustdesk, διαφορετικά η εφαρμογή ενδέχεται να μην είναι σε θέση να βρεί τους απαιτούμενους πόρους. Σημειώστε επίσης ότι άλλες υποεντολές, όπως το `install` ή το `run` δεν υποστηρίζονται επί του παρόντος μέσω αυτής της μεθόδου καθώς θα εγκαταστήσουν ή θα εκτελέσουν το πρόγραμμα εντός του container αντί του κεντρικού υπολογιστή.
|
||||
Βεβαιωθείτε ότι εκτελείτε αυτές τις εντολές από την αρχική διαδρομή του αποθετηρίου του RustDesk, διαφορετικά η εφαρμογή ενδέχεται να μην είναι σε θέση να βρεί τους απαιτούμενους πόρους. Σημειώστε επίσης ότι άλλες υποεντολές, όπως το `install` ή το `run` δεν υποστηρίζονται επί του παρόντος μέσω αυτής της μεθόδου καθώς θα εγκαταστήσουν ή θα εκτελέσουν το πρόγραμμα εντός του container αντί του κεντρικού υπολογιστή.
|
||||
|
||||
## Δομή φακέλων
|
||||
|
||||
|
||||
@@ -116,10 +116,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Válts Wayland-ról X11-re (Xorg)
|
||||
|
||||
A RustDesk nem támogatja a Waylendet. [Itt](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) található egy tutorial amelynek segítségével beállíthatod a Xorg-ot mint alap GNOME session.
|
||||
|
||||
## Hogyan építs Dockerrel
|
||||
|
||||
Kezdjünk a repo clónozásával, majd pedig a Docker container megépítésével:
|
||||
|
||||
@@ -128,37 +128,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Mengubah Wayland ke X11 (Xorg)
|
||||
|
||||
RustDesk tidak mendukung Wayland. Cek [ini](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) untuk mengonfigurasi Xorg sebagai sesi standar di GNOME.
|
||||
|
||||
## Kompatibilitas dengan Wayland
|
||||
|
||||
Sepertinya Wayland tidak memiliki API untuk mengirimkan ketukan tombol ke jendela lain. Maka dari itu, RustDesk menggunakan API dari level yang lebih rendah, lebih tepatnya perangkat `/dev/uinput` (linux kernel level)
|
||||
|
||||
Saat Wayland menjadi sisi yang dikendalikan atau sisi yang sedang diremote, kamu harus memulai dengan cara ini
|
||||
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
|
||||
**Harap Diperhatikan**: Saat Perekaman layar menggunakan Wayland antarmuka (UI) yang ditampilkan akan berbeda. Untuk saat ini RustDesk hanya mendukung org.freedesktop.portal.ScreenCast.
|
||||
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Cara Build dengan Docker
|
||||
|
||||
Mulailah dengan melakukan kloning (clone) repositori dan build dengan docker container:
|
||||
|
||||
@@ -109,11 +109,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Cambiare Wayland in X11 (Xorg)
|
||||
|
||||
RustDesk non supporta Wayland.
|
||||
Controlla [qui](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) per configurare Xorg come sessione predefinita di GNOME.
|
||||
|
||||
## Come compilare con Docker
|
||||
|
||||
Clona il repository e compila i container docker:
|
||||
|
||||
@@ -114,11 +114,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Wayland の場合、X11(Xorg)に変更します
|
||||
|
||||
RustDeskはWaylandをサポートしていません。
|
||||
[こちら](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) を確認して、XorgをデフォルトのGNOMEセッションとして構成します。
|
||||
|
||||
## Dockerでビルドする方法
|
||||
|
||||
リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。
|
||||
|
||||
@@ -112,10 +112,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Wayland 일 경우, X11(Xorg)로 변경
|
||||
|
||||
RustDesk는 Wayland를 지원하지 않습니다. [링크](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)를 확인해서 Xorg 기본값의 GNOME 세션을 구성합니다.
|
||||
|
||||
## Docker에 빌드하는 방법
|
||||
|
||||
레포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
|
||||
|
||||
@@ -103,10 +103,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### വേലാൻഡ് X11 (Xorg) ആയി മാറ്റുക
|
||||
|
||||
RustDesk Wayland-നെ പിന്തുണയ്ക്കുന്നില്ല. സ്ഥിരസ്ഥിതി ഗ്നോം സെഷനായി Xorg കോൺഫിഗർ ചെയ്യുന്നതിന് [ഇത്](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) പരിശോധിക്കുക.
|
||||
|
||||
## ഡോക്കർ ഉപയോഗിച്ച് എങ്ങനെ നിർമ്മിക്കാം
|
||||
|
||||
റെപ്പോസിറ്റോറി ക്ലോണുചെയ്ത് ഡോക്കർ കണ്ടെയ്നർ നിർമ്മിക്കുന്നതിലൂടെ ആരംഭിക്കുക:
|
||||
|
||||
@@ -130,34 +130,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Wissel van Wayland naar X11 (Xorg)
|
||||
|
||||
RustDesk ondersteunt Wayland niet. Lees [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) hoe je Xorg als standaardsessie kunt instellen voor GNOME.
|
||||
|
||||
## Wayland support
|
||||
|
||||
Wayland lijkt geen API te bieden voor het verzenden van toetsaanslagen naar andere vensters. Daarom gebruikt de rustdesk een API van een lager niveau, namelijk het `/dev/uinput` apparaat (Linux kernel niveau).
|
||||
|
||||
Als wayland de gecontroleerde kant is, moet je op de volgende manier beginnen:
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Let op**: Wayland schermopname gebruikt verschillende interfaces. RustDesk ondersteunt momenteel alleen org.freedesktop.portal.ScreenCast.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Bouwen met Docker
|
||||
|
||||
Begin met het klonen van de repository en het bouwen van de docker container:
|
||||
|
||||
@@ -128,34 +128,6 @@ mv libsciter-gtk.so target/debug
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Zmień Wayland na X11 (Xorg)
|
||||
|
||||
RustDesk nie obsługuje Waylanda. Sprawdź [tutaj](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), jak skonfigurować Xorg jako domyślną sesję GNOME.
|
||||
|
||||
## Wspracie Wayland
|
||||
|
||||
Wygląda na to, że Wayland nie wspiera żadnego API do wysyłania naciśnięć klawiszy do innych okien. Dlatego rustdesk używa API z niższego poziomu, urządzenia o nazwie `/dev/uinput` (poziom jądra Linux).
|
||||
|
||||
Gdy po stronie kontrolowanej pracuje Wayland, musisz uruchomić program w następujący sposób:
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Uwaga**: Nagrywanie ekranu Wayland wykorzystuje różne interfejsy. RustDesk obecnie obsługuje tylko org.freedesktop.portal.ScreenCast.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Jak kompilować za pomocą Dockera
|
||||
|
||||
Rozpocznij od sklonowania repozytorium i stworzenia kontenera docker:
|
||||
|
||||
@@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Mude Wayland para X11 (Xorg)
|
||||
|
||||
RustDesk não suporta Wayland. Veja [esse link](https://docs.fedoraproject.org/pt_BR/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar o Xorg como a sessão padrão do GNOME.
|
||||
|
||||
## Como compilar com Docker
|
||||
|
||||
Comece clonando o repositório e montando o container docker:
|
||||
|
||||
@@ -114,10 +114,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Смените Wayland на X11 (Xorg)
|
||||
|
||||
RustDesk не поддерживает Wayland. Смотрите [этот документ](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) для настройки Xorg в качестве сеанса GNOME по умолчанию.
|
||||
|
||||
## Как собрать с помощью Docker
|
||||
|
||||
Начните с клонирования репозитория и создания docker-контейнера:
|
||||
|
||||
@@ -138,34 +138,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Wayland'ı X11 (Xorg) Olarak Değiştirme
|
||||
|
||||
RustDesk, Wayland'ı desteklemez. Xorg'u GNOME oturumu olarak varsayılan olarak ayarlamak için [burayı](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) kontrol edin.
|
||||
|
||||
## Wayland Desteği
|
||||
|
||||
Wayland'ın diğer pencerelere tuş vuruşu göndermek için herhangi bir API sağlamadığı görünmektedir. Bu nedenle, RustDesk daha düşük bir seviyeden, yani Linux çekirdek seviyesindeki `/dev/uinput` cihazının API'sini kullanır.
|
||||
|
||||
Wayland tarafı kontrol edildiğinde, aşağıdaki şekilde başlatmanız gerekir:
|
||||
```bash
|
||||
# uinput servisini başlatın
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Uyarı**: Wayland ekran kaydı farklı arayüzler kullanır. RustDesk şu anda yalnızca org.freedesktop.portal.ScreenCast'ı destekler.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Desteklenmez
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Desteklenir
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Docker ile Derleme Nasıl Yapılır
|
||||
|
||||
Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun:
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Ваш віддалений робочий стіл"><br>
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
|
||||
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> •
|
||||
<a href="#первинні-кроки-для-складання">Складання</a> •
|
||||
<a href="#кроки-для-збірки">Збирання</a> •
|
||||
<a href="#як-зібрати-за-допомогою-docker">Docker</a> •
|
||||
<a href="#структура-файлів">Структура</a> •
|
||||
<a href="#знімки">Знімки</a><br>
|
||||
[<a href="../README.md">English</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-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Нам потрібна ваша допомога для перекладу цього README і <a href="https://github.com/rustdesk/rustdesk/tree/master/src/rustdesk/tree/master/src/lang">RustDesk UI</a> на вашу рідну мову</B>
|
||||
[<a href="../README.md">English</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>
|
||||
<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)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Ще одне програмне забезпечення для віддаленого робочого столу, написане на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) для допомоги на початку роботи.
|
||||
RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](docs/CONTRIBUTING.md), щоб отримати допомогу на початковому етапі.
|
||||
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
[**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**Як працює RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
[**ЗАВАНТАЖЕННЯ ЗАСТОСУНКУ**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**ЗАВАНТАЖИТИ ЗАСТОСУНОК**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**НІЧНІ ЗБІРКИ**](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"
|
||||
@@ -34,38 +36,40 @@ RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIB
|
||||
Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно.
|
||||
| Місцезнаходження | Постачальник | Технічні характеристики |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Німеччина | Hetzner | 2 VCPU / 4GB RAM |
|
||||
| Німеччина | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
|
||||
| Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Якщо у вас уже встановлено VS Code і Docker, ви можете натиснути значок вище, щоб почати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує виcхідний код у том контейнера та розгорне контейнер dev для використання.
|
||||
Якщо у вас уже встановлено VS Code та Docker, ви можете натиснути значок вище, щоб розпочати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує вихідний код у том контейнера та розгорне контейнер dev для використання.
|
||||
|
||||
Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інфо.
|
||||
Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інформації
|
||||
|
||||
## Залежності
|
||||
|
||||
Настільні версії використовують [sciter](https://sciter.com/) для графічного інтерфейсу, завантажте динамічну бібліотеку sciter самостійно.
|
||||
Стільничні версії використовують Flutter чи Sciter (застаріле) для графічного інтерфейсу. Ця інструкція лише для Sciter, оскільки він є більш простим та дружнім для початківців. Перегляньте [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) для збірки версії на Flutter.
|
||||
|
||||
Будь ласка, завантажте динамічну бібліотеку Sciter самостійно.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[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`.
|
||||
|
||||
- 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`
|
||||
|
||||
## [Збирання](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Як зібрати на Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
@@ -76,11 +80,12 @@ sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxc
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
### 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
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
@@ -99,7 +104,7 @@ sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-c
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2023.04.15
|
||||
cd ...
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
@@ -118,7 +123,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Збірка
|
||||
### Збирання
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
@@ -131,10 +136,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Змініть Wayland на X11 (Xorg)
|
||||
|
||||
RustDesk не підтримує Wayland. Дивіться [цей документ](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) для налаштування Xorg як сеансу GNOME за замовчуванням.
|
||||
|
||||
## Як зібрати за допомогою Docker
|
||||
|
||||
Почніть з клонування сховища та створення docker-контейнера:
|
||||
@@ -145,7 +146,7 @@ cd rustdesk
|
||||
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
|
||||
@@ -170,6 +171,7 @@ target/release/rustdesk
|
||||
- **[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)**: специфічне для платформи керування клавіатурою/мишею
|
||||
- **[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)**: графічний інтерфейс користувача
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання
|
||||
|
||||
@@ -116,10 +116,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### Chuyển từ Wayland sang X11 (Xorg)
|
||||
|
||||
RustDesk hiện không hỗ trợ Wayland. Hãy xem [đường linh ở đây](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) cách để cài đặt Xorg làm session mặc định của GNOME.
|
||||
|
||||
## Cách để build sử dụng Docker
|
||||
|
||||
Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer:
|
||||
|
||||
@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
|
||||
|
||||

|
||||
|
||||
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](docs/CONTRIBUTING.md).
|
||||
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
@@ -134,39 +134,6 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
### 把 Wayland 修改成 X11 (Xorg)
|
||||
|
||||
RustDesk 暂时不支持 Wayland,不过正在积极开发中。
|
||||
> [点我](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)
|
||||
查看如何将 Xorg 设置成默认的 GNOME session.
|
||||
|
||||
## Wayland 支持
|
||||
|
||||
Wayland 似乎没有提供任何将按键发送到其他窗口的 API. 因此, RustDesk 使用较低级别的 API, 即 `/dev/uinput` devices (Linux kernal level).
|
||||
|
||||
当 Wayland 是受控方时,您必须以下列方式开始操作:
|
||||
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
|
||||
**Notice**: Wayland 屏幕录制使用不同的接口. RustDesk 目前只支持 org.freedesktop.portal.ScreenCast.
|
||||
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## 使用 Docker 编译
|
||||
|
||||
克隆版本库并构建 Docker 容器:
|
||||
|
||||
9
docs/SECURITY-JP.md
Normal file
9
docs/SECURITY-JP.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# セキュリティポリシー
|
||||
|
||||
## 脆弱性の報告
|
||||
|
||||
私たちはプロジェクトのセキュリティを非常に重視しています。私たちは、すべてのユーザーが脆弱性を発見した場合、私たちに報告することを奨励しています。
|
||||
RustDesk プロジェクトにセキュリティの脆弱性を発見した場合は、info@rustdesk.com までメールで責任を持って報告してください。
|
||||
|
||||
現時点では、バグ報奨金制度はありません。私たちは大きな問題を解決しようとしている小さなチームです。コミュニティ全体のために安全なアプリケーションを作り続けることができるよう、
|
||||
責任を持って脆弱性を報告してください。
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "rustdesk",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"bsdtar -zxvf rustdesk-1.2.3.deb",
|
||||
"bsdtar -zxvf rustdesk-1.2.4.deb",
|
||||
"tar -xvf ./data.tar.xz",
|
||||
"cp -r ./usr/* /app/",
|
||||
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
|
||||
@@ -26,7 +26,7 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../rustdesk-1.2.3.deb"
|
||||
"path": "../rustdesk-1.2.4.deb"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
|
||||
@@ -13,4 +13,4 @@ A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
samples and guidance on mobile development, and a full API reference.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import com.google.protobuf.gradle.*
|
||||
plugins {
|
||||
id "com.google.protobuf" version "0.9.4"
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
@@ -31,10 +36,33 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.20.1'
|
||||
}
|
||||
|
||||
generateProtoTasks {
|
||||
all().configureEach { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
||||
main.proto.srcDirs += '../../../libs/hbb_common/protos'
|
||||
main.proto.includes += "message.proto"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -65,6 +93,7 @@ android {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig signingConfigs.release
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +104,7 @@ flutter {
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.media:media:1.6.0"
|
||||
implementation 'com.github.getActivity:XXPermissions:16.2'
|
||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||
}
|
||||
|
||||
|
||||
4
flutter/android/app/proguard-rules
Normal file
4
flutter/android/app/proguard-rules
Normal file
@@ -0,0 +1,4 @@
|
||||
# Keep class members from protobuf generated code.
|
||||
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
@@ -61,6 +61,14 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Intent for deep linking-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="rustdesk" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -81,4 +89,4 @@
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -10,12 +10,27 @@ import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.EditText
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.graphics.Rect
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.*
|
||||
import java.lang.Character
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import hbb.MessageOuterClass.KeyEvent
|
||||
import hbb.MessageOuterClass.KeyboardMode
|
||||
import hbb.KeyEventConverter
|
||||
|
||||
const val LIFT_DOWN = 9
|
||||
const val LIFT_MOVE = 8
|
||||
@@ -58,6 +73,8 @@ class InputService : AccessibilityService() {
|
||||
private var isWheelActionsPolling = false
|
||||
private var isWaitingLongPress = false
|
||||
|
||||
private var fakeEditTextForTextStateCalculation: EditText? = null
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
||||
val x = max(0, _x)
|
||||
@@ -252,9 +269,296 @@ class InputService : AccessibilityService() {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun onKeyEvent(data: ByteArray) {
|
||||
val keyEvent = KeyEvent.parseFrom(data)
|
||||
val keyboardMode = keyEvent.getMode()
|
||||
|
||||
var textToCommit: String? = null
|
||||
|
||||
if (keyboardMode == KeyboardMode.Legacy) {
|
||||
if (keyEvent.hasChr() && keyEvent.getDown()) {
|
||||
val chr = keyEvent.getChr()
|
||||
if (chr != null) {
|
||||
textToCommit = String(Character.toChars(chr))
|
||||
}
|
||||
}
|
||||
} else if (keyboardMode == KeyboardMode.Translate) {
|
||||
if (keyEvent.hasSeq() && keyEvent.getDown()) {
|
||||
val seq = keyEvent.getSeq()
|
||||
if (seq != null) {
|
||||
textToCommit = seq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
getInputMethod()?.let { inputMethod ->
|
||||
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
||||
if (textToCommit != null) {
|
||||
textToCommit?.let { text ->
|
||||
inputConnection.commitText(text, 1, null)
|
||||
}
|
||||
} else {
|
||||
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event ->
|
||||
inputConnection.sendKeyEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
handler.post {
|
||||
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event ->
|
||||
val possibleNodes = possibleAccessibiltyNodes()
|
||||
Log.d(logTag, "possibleNodes:$possibleNodes")
|
||||
for (item in possibleNodes) {
|
||||
val success = trySendKeyEvent(event, item, textToCommit)
|
||||
if (success) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
if (list.contains(node)) {
|
||||
return
|
||||
}
|
||||
list.add(node)
|
||||
}
|
||||
|
||||
private fun findChildNode(node: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
|
||||
if (node == null) {
|
||||
return null
|
||||
}
|
||||
if (node.isEditable() && node.isFocusable()) {
|
||||
return node
|
||||
}
|
||||
val childCount = node.getChildCount()
|
||||
for (i in 0 until childCount) {
|
||||
val child = node.getChild(i)
|
||||
if (child != null) {
|
||||
if (child.isEditable() && child.isFocusable()) {
|
||||
return child
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 33) {
|
||||
child.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i in 0 until childCount) {
|
||||
val child = node.getChild(i)
|
||||
if (child != null) {
|
||||
val result = findChildNode(child)
|
||||
if (Build.VERSION.SDK_INT < 33) {
|
||||
if (child != result) {
|
||||
child.recycle()
|
||||
}
|
||||
}
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun possibleAccessibiltyNodes(): LinkedList<AccessibilityNodeInfo> {
|
||||
val linkedList = LinkedList<AccessibilityNodeInfo>()
|
||||
val latestList = LinkedList<AccessibilityNodeInfo>()
|
||||
|
||||
val focusInput = findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
|
||||
var focusAccessibilityInput = findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
|
||||
|
||||
val rootInActiveWindow = getRootInActiveWindow()
|
||||
|
||||
Log.d(logTag, "focusInput:$focusInput focusAccessibilityInput:$focusAccessibilityInput rootInActiveWindow:$rootInActiveWindow")
|
||||
|
||||
if (focusInput != null) {
|
||||
if (focusInput.isFocusable() && focusInput.isEditable()) {
|
||||
insertAccessibilityNode(linkedList, focusInput)
|
||||
} else {
|
||||
insertAccessibilityNode(latestList, focusInput)
|
||||
}
|
||||
}
|
||||
|
||||
if (focusAccessibilityInput != null) {
|
||||
if (focusAccessibilityInput.isFocusable() && focusAccessibilityInput.isEditable()) {
|
||||
insertAccessibilityNode(linkedList, focusAccessibilityInput)
|
||||
} else {
|
||||
insertAccessibilityNode(latestList, focusAccessibilityInput)
|
||||
}
|
||||
}
|
||||
|
||||
val childFromFocusInput = findChildNode(focusInput)
|
||||
Log.d(logTag, "childFromFocusInput:$childFromFocusInput")
|
||||
|
||||
if (childFromFocusInput != null) {
|
||||
insertAccessibilityNode(linkedList, childFromFocusInput)
|
||||
}
|
||||
|
||||
val childFromFocusAccessibilityInput = findChildNode(focusAccessibilityInput)
|
||||
if (childFromFocusAccessibilityInput != null) {
|
||||
insertAccessibilityNode(linkedList, childFromFocusAccessibilityInput)
|
||||
}
|
||||
Log.d(logTag, "childFromFocusAccessibilityInput:$childFromFocusAccessibilityInput")
|
||||
|
||||
if (rootInActiveWindow != null) {
|
||||
insertAccessibilityNode(linkedList, rootInActiveWindow)
|
||||
}
|
||||
|
||||
for (item in latestList) {
|
||||
insertAccessibilityNode(linkedList, item)
|
||||
}
|
||||
|
||||
return linkedList
|
||||
}
|
||||
|
||||
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
||||
node.refresh()
|
||||
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
||||
this.fakeEditTextForTextStateCalculation?.setText(null)
|
||||
|
||||
val text = node.getText()
|
||||
var isShowingHint = false
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
isShowingHint = node.isShowingHintText()
|
||||
}
|
||||
|
||||
var textSelectionStart = node.textSelectionStart
|
||||
var textSelectionEnd = node.textSelectionEnd
|
||||
|
||||
if (text != null) {
|
||||
if (textSelectionStart > text.length) {
|
||||
textSelectionStart = text.length
|
||||
}
|
||||
if (textSelectionEnd > text.length) {
|
||||
textSelectionEnd = text.length
|
||||
}
|
||||
if (textSelectionStart > textSelectionEnd) {
|
||||
textSelectionStart = textSelectionEnd
|
||||
}
|
||||
}
|
||||
|
||||
var success = false
|
||||
|
||||
Log.d(logTag, "existing text:$text textToCommit:$textToCommit textSelectionStart:$textSelectionStart textSelectionEnd:$textSelectionEnd")
|
||||
|
||||
if (textToCommit != null) {
|
||||
if ((textSelectionStart == -1) || (textSelectionEnd == -1)) {
|
||||
val newText = textToCommit
|
||||
this.fakeEditTextForTextStateCalculation?.setText(newText)
|
||||
success = updateTextForAccessibilityNode(node)
|
||||
} else if (text != null) {
|
||||
this.fakeEditTextForTextStateCalculation?.setText(text)
|
||||
this.fakeEditTextForTextStateCalculation?.setSelection(
|
||||
textSelectionStart,
|
||||
textSelectionEnd
|
||||
)
|
||||
this.fakeEditTextForTextStateCalculation?.text?.insert(textSelectionStart, textToCommit)
|
||||
success = updateTextAndSelectionForAccessibiltyNode(node)
|
||||
}
|
||||
} else {
|
||||
if (isShowingHint) {
|
||||
this.fakeEditTextForTextStateCalculation?.setText(null)
|
||||
} else {
|
||||
this.fakeEditTextForTextStateCalculation?.setText(text)
|
||||
}
|
||||
if (textSelectionStart != -1 && textSelectionEnd != -1) {
|
||||
Log.d(logTag, "setting selection $textSelectionStart $textSelectionEnd")
|
||||
this.fakeEditTextForTextStateCalculation?.setSelection(
|
||||
textSelectionStart,
|
||||
textSelectionEnd
|
||||
)
|
||||
}
|
||||
|
||||
this.fakeEditTextForTextStateCalculation?.let {
|
||||
// This is essiential to make sure layout object is created. OnKeyDown may not work if layout is not created.
|
||||
val rect = Rect()
|
||||
node.getBoundsInScreen(rect)
|
||||
|
||||
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
||||
it.onPreDraw()
|
||||
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
|
||||
val succ = it.onKeyDown(event.getKeyCode(), event)
|
||||
Log.d(logTag, "onKeyDown $succ")
|
||||
} else if (event.action == android.view.KeyEvent.ACTION_UP) {
|
||||
val success = it.onKeyUp(event.getKeyCode(), event)
|
||||
Log.d(logTag, "keyup $success")
|
||||
} else {}
|
||||
}
|
||||
|
||||
success = updateTextAndSelectionForAccessibiltyNode(node)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun updateTextForAccessibilityNode(node: AccessibilityNodeInfo): Boolean {
|
||||
var success = false
|
||||
this.fakeEditTextForTextStateCalculation?.text?.let {
|
||||
val arguments = Bundle()
|
||||
arguments.putCharSequence(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
|
||||
it.toString()
|
||||
)
|
||||
success = node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun updateTextAndSelectionForAccessibiltyNode(node: AccessibilityNodeInfo): Boolean {
|
||||
var success = updateTextForAccessibilityNode(node)
|
||||
|
||||
if (success) {
|
||||
val selectionStart = this.fakeEditTextForTextStateCalculation?.selectionStart
|
||||
val selectionEnd = this.fakeEditTextForTextStateCalculation?.selectionEnd
|
||||
|
||||
if (selectionStart != null && selectionEnd != null) {
|
||||
val arguments = Bundle()
|
||||
arguments.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT,
|
||||
selectionStart
|
||||
)
|
||||
arguments.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT,
|
||||
selectionEnd
|
||||
)
|
||||
success = node.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments)
|
||||
Log.d(logTag, "Update selection to $selectionStart $selectionEnd success:$success")
|
||||
}
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||
}
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
ctx = this
|
||||
val info = AccessibilityServiceInfo()
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||
} else {
|
||||
info.flags = FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||
}
|
||||
setServiceInfo(info)
|
||||
fakeEditTextForTextStateCalculation = EditText(this)
|
||||
// Size here doesn't matter, we won't show this view.
|
||||
fakeEditTextForTextStateCalculation?.layoutParams = LayoutParams(100, 100)
|
||||
fakeEditTextForTextStateCalculation?.onPreDraw()
|
||||
val layout = fakeEditTextForTextStateCalculation?.getLayout()
|
||||
Log.d(logTag, "fakeEditTextForTextStateCalculation layout:$layout")
|
||||
Log.d(logTag, "onServiceConnected!")
|
||||
}
|
||||
|
||||
@@ -263,7 +567,5 @@ class InputService : AccessibilityService() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
|
||||
|
||||
override fun onInterrupt() {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package hbb;
|
||||
import android.view.KeyEvent
|
||||
import android.view.KeyCharacterMap
|
||||
import hbb.MessageOuterClass.KeyboardMode
|
||||
import hbb.MessageOuterClass.ControlKey
|
||||
|
||||
object KeyEventConverter {
|
||||
fun toAndroidKeyEvent(keyEventProto: hbb.MessageOuterClass.KeyEvent): KeyEvent {
|
||||
var chrValue = 0
|
||||
var modifiers = 0
|
||||
|
||||
val keyboardMode = keyEventProto.getMode()
|
||||
|
||||
if (keyEventProto.hasChr()) {
|
||||
if (keyboardMode == KeyboardMode.Map || keyboardMode == KeyboardMode.Translate) {
|
||||
chrValue = keyEventProto.getChr()
|
||||
} else {
|
||||
chrValue = convertUnicodeToKeyCode(keyEventProto.getChr() as Int)
|
||||
}
|
||||
} else if (keyEventProto.hasControlKey()) {
|
||||
chrValue = convertControlKeyToKeyCode(keyEventProto.getControlKey())
|
||||
}
|
||||
|
||||
var modifiersList = keyEventProto.getModifiersList()
|
||||
|
||||
if (modifiersList != null) {
|
||||
for (modifier in keyEventProto.getModifiersList()) {
|
||||
val modifierValue = convertModifier(modifier)
|
||||
modifiers = modifiers or modifierValue
|
||||
}
|
||||
}
|
||||
|
||||
var action = 0
|
||||
if (keyEventProto.getDown()) {
|
||||
action = KeyEvent.ACTION_DOWN
|
||||
} else {
|
||||
action = KeyEvent.ACTION_UP
|
||||
}
|
||||
|
||||
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
||||
}
|
||||
|
||||
private fun convertModifier(controlKey: hbb.MessageOuterClass.ControlKey): Int {
|
||||
// Add logic to map ControlKey values to Android KeyEvent key codes.
|
||||
// You'll need to provide the mapping for each key.
|
||||
return when (controlKey) {
|
||||
ControlKey.Alt -> KeyEvent.META_ALT_ON
|
||||
ControlKey.Control -> KeyEvent.META_CTRL_ON
|
||||
ControlKey.CapsLock -> KeyEvent.META_CAPS_LOCK_ON
|
||||
ControlKey.Meta -> KeyEvent.META_META_ON
|
||||
ControlKey.NumLock -> KeyEvent.META_NUM_LOCK_ON
|
||||
ControlKey.RShift -> KeyEvent.META_SHIFT_RIGHT_ON
|
||||
ControlKey.Shift -> KeyEvent.META_SHIFT_ON
|
||||
ControlKey.RAlt -> KeyEvent.META_ALT_RIGHT_ON
|
||||
ControlKey.RControl -> KeyEvent.META_CTRL_RIGHT_ON
|
||||
else -> 0 // Default to unknown.
|
||||
}
|
||||
}
|
||||
|
||||
private val tag = "KeyEventConverter"
|
||||
|
||||
private fun convertUnicodeToKeyCode(unicode: Int): Int {
|
||||
val charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD)
|
||||
android.util.Log.d(tag, "unicode: $unicode")
|
||||
val events = charMap.getEvents(charArrayOf(unicode.toChar()))
|
||||
if (events != null && events.size > 0) {
|
||||
android.util.Log.d(tag, "keycode ${events[0].keyCode}")
|
||||
return events[0].keyCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun convertControlKeyToKeyCode(controlKey: hbb.MessageOuterClass.ControlKey): Int {
|
||||
// Add logic to map ControlKey values to Android KeyEvent key codes.
|
||||
// You'll need to provide the mapping for each key.
|
||||
return when (controlKey) {
|
||||
ControlKey.Alt -> KeyEvent.KEYCODE_ALT_LEFT
|
||||
ControlKey.Backspace -> KeyEvent.KEYCODE_DEL
|
||||
ControlKey.Control -> KeyEvent.KEYCODE_CTRL_LEFT
|
||||
ControlKey.CapsLock -> KeyEvent.KEYCODE_CAPS_LOCK
|
||||
ControlKey.Meta -> KeyEvent.KEYCODE_META_LEFT
|
||||
ControlKey.NumLock -> KeyEvent.KEYCODE_NUM_LOCK
|
||||
ControlKey.RShift -> KeyEvent.KEYCODE_SHIFT_RIGHT
|
||||
ControlKey.Shift -> KeyEvent.KEYCODE_SHIFT_LEFT
|
||||
ControlKey.RAlt -> KeyEvent.KEYCODE_ALT_RIGHT
|
||||
ControlKey.RControl -> KeyEvent.KEYCODE_CTRL_RIGHT
|
||||
ControlKey.DownArrow -> KeyEvent.KEYCODE_DPAD_DOWN
|
||||
ControlKey.LeftArrow -> KeyEvent.KEYCODE_DPAD_LEFT
|
||||
ControlKey.RightArrow -> KeyEvent.KEYCODE_DPAD_RIGHT
|
||||
ControlKey.UpArrow -> KeyEvent.KEYCODE_DPAD_UP
|
||||
ControlKey.End -> KeyEvent.KEYCODE_MOVE_END
|
||||
ControlKey.Home -> KeyEvent.KEYCODE_MOVE_HOME
|
||||
ControlKey.PageUp -> KeyEvent.KEYCODE_PAGE_UP
|
||||
ControlKey.PageDown -> KeyEvent.KEYCODE_PAGE_DOWN
|
||||
ControlKey.Insert -> KeyEvent.KEYCODE_INSERT
|
||||
ControlKey.Escape -> KeyEvent.KEYCODE_ESCAPE
|
||||
ControlKey.F1 -> KeyEvent.KEYCODE_F1
|
||||
ControlKey.F2 -> KeyEvent.KEYCODE_F2
|
||||
ControlKey.F3 -> KeyEvent.KEYCODE_F3
|
||||
ControlKey.F4 -> KeyEvent.KEYCODE_F4
|
||||
ControlKey.F5 -> KeyEvent.KEYCODE_F5
|
||||
ControlKey.F6 -> KeyEvent.KEYCODE_F6
|
||||
ControlKey.F7 -> KeyEvent.KEYCODE_F7
|
||||
ControlKey.F8 -> KeyEvent.KEYCODE_F8
|
||||
ControlKey.F9 -> KeyEvent.KEYCODE_F9
|
||||
ControlKey.F10 -> KeyEvent.KEYCODE_F10
|
||||
ControlKey.F11 -> KeyEvent.KEYCODE_F11
|
||||
ControlKey.F12 -> KeyEvent.KEYCODE_F12
|
||||
ControlKey.Space -> KeyEvent.KEYCODE_SPACE
|
||||
ControlKey.Tab -> KeyEvent.KEYCODE_TAB
|
||||
ControlKey.Return -> KeyEvent.KEYCODE_ENTER
|
||||
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
||||
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
||||
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
||||
else -> 0 // Default to unknown.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,6 @@ import java.nio.ByteBuffer
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
const val DEFAULT_NOTIFY_TITLE = "RustDesk"
|
||||
const val DEFAULT_NOTIFY_TEXT = "Service is running"
|
||||
const val DEFAULT_NOTIFY_ID = 1
|
||||
@@ -94,6 +93,12 @@ class MainService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun rustKeyEventInput(input: ByteArray) {
|
||||
InputService.ctx?.onKeyEvent(input)
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun rustGetByName(name: String): String {
|
||||
return when (name) {
|
||||
@@ -206,6 +211,7 @@ class MainService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(logTag,"MainService onCreate")
|
||||
init(this)
|
||||
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
serviceLooper = looper
|
||||
@@ -310,7 +316,6 @@ class MainService : Service() {
|
||||
mediaProjection =
|
||||
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
|
||||
checkMediaPermission()
|
||||
init(this)
|
||||
_isReady = true
|
||||
} ?: let {
|
||||
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accessibilityEventTypes="typeWindowsChanged"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:accessibilityFlags="flagDefault"
|
||||
android:notificationTimeout="50"
|
||||
android:description="@string/accessibility_service_description"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.9.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build libyuv / opus / libvpx / oboe for Android
|
||||
set -e -o pipefail
|
||||
|
||||
ANDROID_ABI=$1
|
||||
|
||||
# Build RustDesk dependencies for Android using vcpkg.json
|
||||
# Required:
|
||||
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
|
||||
# 2. vcpkg initialized
|
||||
# 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`)
|
||||
# 3. ndk, version: r25c or newer
|
||||
|
||||
if [ -z "$ANDROID_NDK" ]; then
|
||||
echo "Failed! Please set ANDROID_NDK"
|
||||
if [ -z "$ANDROID_NDK_HOME" ]; then
|
||||
echo "Failed! Please set ANDROID_NDK_HOME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -18,107 +22,66 @@ fi
|
||||
|
||||
API_LEVEL="21"
|
||||
|
||||
# Get directory of this script
|
||||
|
||||
SCRIPTDIR="$(readlink -f "$0")"
|
||||
SCRIPTDIR="$(dirname "$SCRIPTDIR")"
|
||||
|
||||
# Check if vcpkg.json is one level up - in root directory of RD
|
||||
|
||||
if [ ! -f "$SCRIPTDIR/../vcpkg.json" ]; then
|
||||
echo "Failed! Please check where vcpkg.json is!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# NDK llvm toolchain
|
||||
|
||||
HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/llvm/prebuilt/`
|
||||
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
|
||||
|
||||
function build {
|
||||
ANDROID_ABI=$1
|
||||
VCPKG_TARGET=$2
|
||||
NDK_LLVM_TARGET=$3
|
||||
LIBVPX_TARGET=$4
|
||||
|
||||
PREFIX=$VCPKG_ROOT/installed/$VCPKG_TARGET/
|
||||
case "$ANDROID_ABI" in
|
||||
arm64-v8a)
|
||||
ABI=aarch64-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=arm64-android
|
||||
;;
|
||||
armeabi-v7a)
|
||||
ABI=armv7a-linux-androideabi$API_LEVEL
|
||||
VCPKG_TARGET=arm-neon-android
|
||||
;;
|
||||
x86_64)
|
||||
ABI=x86_64-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=x64-android
|
||||
;;
|
||||
x86)
|
||||
ABI=i686-linux-android$API_LEVEL
|
||||
VCPKG_TARGET=x86-android
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: ANDROID_ABI must be one of: arm64-v8a, armeabi-v7a, x86_64, x86" >&2
|
||||
return 1
|
||||
esac
|
||||
|
||||
# 1
|
||||
echo "*** [$ANDROID_ABI][Start] Build opus / libyuv from vcpkg"
|
||||
export ANDROID_NDK_HOME=$ANDROID_NDK
|
||||
pushd $VCPKG_ROOT
|
||||
$VCPKG_ROOT/vcpkg install opus --triplet $VCPKG_TARGET
|
||||
$VCPKG_ROOT/vcpkg install libyuv --triplet $VCPKG_TARGET
|
||||
echo "*** [$ANDROID_ABI][Start] Build and install vcpkg dependencies"
|
||||
pushd "$SCRIPTDIR/.."
|
||||
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
|
||||
popd
|
||||
echo "*** [$ANDROID_ABI][Finished] Build opus / libyuv from vcpkg"
|
||||
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
|
||||
|
||||
# 2
|
||||
echo "*** [$ANDROID_ABI][Start] Build libvpx"
|
||||
pushd build/libvpx
|
||||
export AR=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ar
|
||||
export AS=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-as
|
||||
export LD=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ld.gold # if ndk < 22, use aarch64-linux-android-ld
|
||||
export RANLIB=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-ranlib
|
||||
export STRIP=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}-strip
|
||||
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
|
||||
echo "*** [Start] Move arm-neon-android to arm-android"
|
||||
|
||||
if [ $NDK_LLVM_TARGET == "arm-linux-androideabi" ]
|
||||
then
|
||||
export CC=$TOOLCHAIN/bin/armv7a-linux-androideabi${API_LEVEL}-clang
|
||||
export CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi${API_LEVEL}-clang++
|
||||
else
|
||||
export CC=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}${API_LEVEL}-clang
|
||||
export CXX=$TOOLCHAIN/bin/${NDK_LLVM_TARGET}${API_LEVEL}-clang++
|
||||
fi
|
||||
make clean
|
||||
./configure --target=$LIBVPX_TARGET \
|
||||
--enable-pic
|
||||
--disable-webm-io \
|
||||
--disable-unit-tests \
|
||||
--disable-examples \
|
||||
--disable-libyuv \
|
||||
--disable-postproc \
|
||||
--disable-tools \
|
||||
--disable-docs \
|
||||
--prefix=$PREFIX
|
||||
make -j5
|
||||
make install
|
||||
mv "$VCPKG_ROOT/installed/arm-neon-android" "$VCPKG_ROOT/installed/arm-android"
|
||||
|
||||
popd
|
||||
echo "*** [$ANDROID_ABI][Finished] Build libvpx"
|
||||
|
||||
# 3
|
||||
echo "*** [$ANDROID_ABI][Start] Build oboe"
|
||||
pushd build/oboe
|
||||
make clean
|
||||
cmake -DBUILD_SHARED_LIBS=true \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DANDROID_TOOLCHAIN=clang \
|
||||
-DANDROID_STL=c++_shared \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=$PREFIX \
|
||||
-DANDROID_ABI=$ANDROID_ABI \
|
||||
-DANDROID_PLATFORM=android-$API_LEVEL
|
||||
make -j5
|
||||
make install
|
||||
mv $PREFIX/lib/$ANDROID_ABI/liboboe.a $PREFIX/lib/
|
||||
popd
|
||||
echo "*** [$ANDROID_ABI][Finished] Build oboe"
|
||||
|
||||
echo "*** [$ANDROID_ABI][All Finished]"
|
||||
echo "*** [Finished] Move arm-neon-android to arm-android"
|
||||
fi
|
||||
}
|
||||
|
||||
git clone -b v1.11.0 --depth=1 https://github.com/webmproject/libvpx.git build/libvpx
|
||||
git clone -b 1.6.1 --depth=1 https://github.com/google/oboe build/oboe
|
||||
patch -N -d build/oboe -p1 < ../src/oboe.patch
|
||||
|
||||
# VCPKG_TARGET ANDROID_ABI
|
||||
# arm64-android arm64-v8a
|
||||
# arm-android armeabi-v7a
|
||||
# x64-android x86_64
|
||||
# x86-android x86
|
||||
|
||||
# NDK_LLVM_TARGET
|
||||
# aarch64-linux-android
|
||||
# arm-linux-androideabi
|
||||
# x86_64-linux-android
|
||||
# i686-linux-android
|
||||
|
||||
# LIBVPX_TARGET :
|
||||
# arm64-android-gcc
|
||||
# armv7-android-gcc
|
||||
# x86_64-android-gcc
|
||||
# x86-android-gcc
|
||||
|
||||
# args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET
|
||||
build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc
|
||||
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
|
||||
|
||||
# rm -rf build/libvpx
|
||||
# rm -rf build/oboe
|
||||
if [ ! -z "$ANDROID_ABI" ]; then
|
||||
build "$ANDROID_ABI"
|
||||
else
|
||||
echo "Usage: build-android-deps.sh <ANDROID-ABI>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
# platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
platform :ios, '11.0'
|
||||
platform :ios, '12.0'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
|
||||
@@ -38,9 +38,6 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner (5.0.11)
|
||||
@@ -52,12 +49,12 @@ PODS:
|
||||
- qr_code_scanner (0.2.0):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- SDWebImage (5.15.5):
|
||||
- SDWebImage/Core (= 5.15.5)
|
||||
- SDWebImage/Core (5.15.5)
|
||||
- sqflite (0.0.2):
|
||||
- SDWebImage (5.18.11):
|
||||
- SDWebImage/Core (= 5.18.11)
|
||||
- SDWebImage/Core (5.18.11)
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- FlutterMacOS
|
||||
- SwiftyGif (5.4.4)
|
||||
- uni_links (0.0.1):
|
||||
- Flutter
|
||||
@@ -65,7 +62,8 @@ PODS:
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- wakelock (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -77,17 +75,16 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- uni_links (from `.symlinks/plugins/uni_links/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- FMDB
|
||||
- MTBBarcodeScanner
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
@@ -110,37 +107,36 @@ EXTERNAL SOURCES:
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
uni_links:
|
||||
:path: ".symlinks/plugins/uni_links/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||
wakelock:
|
||||
:path: ".symlinks/plugins/wakelock/ios"
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
|
||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
|
||||
PODFILE CHECKSUM: 2aff76ba0ac13439479560d1d03e9b4479f5c9e1
|
||||
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@@ -347,7 +347,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -394,8 +394,6 @@
|
||||
"-framework",
|
||||
"\"DKPhotoGallery\"",
|
||||
"-framework",
|
||||
"\"FMDB\"",
|
||||
"-framework",
|
||||
"\"Foundation\"",
|
||||
"-framework",
|
||||
"\"ImageIO\"",
|
||||
@@ -434,7 +432,7 @@
|
||||
"-framework",
|
||||
"\"video_player_avfoundation\"",
|
||||
"-framework",
|
||||
"\"wakelock\"",
|
||||
"\"wakelock_plus\"",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -493,7 +491,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -543,7 +541,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -592,8 +590,6 @@
|
||||
"-framework",
|
||||
"\"DKPhotoGallery\"",
|
||||
"-framework",
|
||||
"\"FMDB\"",
|
||||
"-framework",
|
||||
"\"Foundation\"",
|
||||
"-framework",
|
||||
"\"ImageIO\"",
|
||||
@@ -632,7 +628,7 @@
|
||||
"-framework",
|
||||
"\"video_player_avfoundation\"",
|
||||
"-framework",
|
||||
"\"wakelock\"",
|
||||
"\"wakelock_plus\"",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -682,8 +678,6 @@
|
||||
"-framework",
|
||||
"\"DKPhotoGallery\"",
|
||||
"-framework",
|
||||
"\"FMDB\"",
|
||||
"-framework",
|
||||
"\"Foundation\"",
|
||||
"-framework",
|
||||
"\"ImageIO\"",
|
||||
@@ -722,7 +716,7 @@
|
||||
"-framework",
|
||||
"\"video_player_avfoundation\"",
|
||||
"-framework",
|
||||
"\"wakelock\"",
|
||||
"\"wakelock_plus\"",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -14,6 +14,6 @@ import Flutter
|
||||
|
||||
public func dummyMethodToEnforceBundling() {
|
||||
dummy_method_to_enforce_bundling();
|
||||
session_get_rgba(nil);
|
||||
session_get_rgba(nil, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,21 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.carriez.rustdesk</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>rustdesk</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
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/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
@@ -52,6 +53,9 @@ int androidVersion = 0;
|
||||
int windowsBuildNumber = 0;
|
||||
DesktopType? desktopType;
|
||||
|
||||
bool get isMainDesktopWindow =>
|
||||
desktopType == DesktopType.main || desktopType == DesktopType.cm;
|
||||
|
||||
/// Check if the app is running with single view mode.
|
||||
bool isSingleViewApp() {
|
||||
return desktopType == DesktopType.cm;
|
||||
@@ -97,59 +101,83 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
const ColorThemeExtension({
|
||||
required this.border,
|
||||
required this.border2,
|
||||
required this.border3,
|
||||
required this.highlight,
|
||||
required this.drag_indicator,
|
||||
required this.shadow,
|
||||
required this.errorBannerBg,
|
||||
required this.me,
|
||||
required this.toastBg,
|
||||
required this.toastText,
|
||||
required this.divider,
|
||||
});
|
||||
|
||||
final Color? border;
|
||||
final Color? border2;
|
||||
final Color? border3;
|
||||
final Color? highlight;
|
||||
final Color? drag_indicator;
|
||||
final Color? shadow;
|
||||
final Color? errorBannerBg;
|
||||
final Color? me;
|
||||
final Color? toastBg;
|
||||
final Color? toastText;
|
||||
final Color? divider;
|
||||
|
||||
static final light = ColorThemeExtension(
|
||||
border: Color(0xFFCCCCCC),
|
||||
border2: Color(0xFFBBBBBB),
|
||||
border3: Colors.black26,
|
||||
highlight: Color(0xFFE5E5E5),
|
||||
drag_indicator: Colors.grey[800],
|
||||
shadow: Colors.black,
|
||||
errorBannerBg: Color(0xFFFDEEEB),
|
||||
me: Colors.green,
|
||||
toastBg: Colors.black.withOpacity(0.6),
|
||||
toastText: Colors.white,
|
||||
divider: Colors.black38,
|
||||
);
|
||||
|
||||
static final dark = ColorThemeExtension(
|
||||
border: Color(0xFF555555),
|
||||
border2: Color(0xFFE5E5E5),
|
||||
border3: Colors.white24,
|
||||
highlight: Color(0xFF3F3F3F),
|
||||
drag_indicator: Colors.grey,
|
||||
shadow: Colors.grey,
|
||||
errorBannerBg: Color(0xFF470F2D),
|
||||
me: Colors.greenAccent,
|
||||
toastBg: Colors.white.withOpacity(0.6),
|
||||
toastText: Colors.black,
|
||||
divider: Colors.white38,
|
||||
);
|
||||
|
||||
@override
|
||||
ThemeExtension<ColorThemeExtension> copyWith({
|
||||
Color? border,
|
||||
Color? border2,
|
||||
Color? border3,
|
||||
Color? highlight,
|
||||
Color? drag_indicator,
|
||||
Color? shadow,
|
||||
Color? errorBannerBg,
|
||||
Color? me,
|
||||
Color? toastBg,
|
||||
Color? toastText,
|
||||
Color? divider,
|
||||
}) {
|
||||
return ColorThemeExtension(
|
||||
border: border ?? this.border,
|
||||
border2: border2 ?? this.border2,
|
||||
border3: border3 ?? this.border3,
|
||||
highlight: highlight ?? this.highlight,
|
||||
drag_indicator: drag_indicator ?? this.drag_indicator,
|
||||
shadow: shadow ?? this.shadow,
|
||||
errorBannerBg: errorBannerBg ?? this.errorBannerBg,
|
||||
me: me ?? this.me,
|
||||
toastBg: toastBg ?? this.toastBg,
|
||||
toastText: toastText ?? this.toastText,
|
||||
divider: divider ?? this.divider,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,11 +190,15 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
return ColorThemeExtension(
|
||||
border: Color.lerp(border, other.border, t),
|
||||
border2: Color.lerp(border2, other.border2, t),
|
||||
border3: Color.lerp(border3, other.border3, t),
|
||||
highlight: Color.lerp(highlight, other.highlight, t),
|
||||
drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t),
|
||||
shadow: Color.lerp(shadow, other.shadow, t),
|
||||
errorBannerBg: Color.lerp(shadow, other.errorBannerBg, t),
|
||||
me: Color.lerp(shadow, other.me, t),
|
||||
toastBg: Color.lerp(shadow, other.toastBg, t),
|
||||
toastText: Color.lerp(shadow, other.toastText, t),
|
||||
divider: Color.lerp(shadow, other.divider, t),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -186,10 +218,6 @@ class MyTheme {
|
||||
static const Color dark = Colors.black87;
|
||||
static const Color button = Color(0xFF2C8CFF);
|
||||
static const Color hoverBorder = Color(0xFF999999);
|
||||
static const Color bordDark = Colors.white24;
|
||||
static const Color bordLight = Colors.black26;
|
||||
static const Color dividerDark = Colors.white38;
|
||||
static const Color dividerLight = Colors.black38;
|
||||
|
||||
// ListTile
|
||||
static const ListTileThemeData listTileTheme = ListTileThemeData(
|
||||
@@ -298,6 +326,8 @@ class MyTheme {
|
||||
);
|
||||
|
||||
static ThemeData lightTheme = ThemeData(
|
||||
// https://stackoverflow.com/questions/77537315/after-upgrading-to-flutter-3-16-the-app-bar-background-color-button-size-and
|
||||
useMaterial3: false,
|
||||
brightness: Brightness.light,
|
||||
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
@@ -376,6 +406,13 @@ class MyTheme {
|
||||
MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))),
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: Colors.blue, secondary: accent, background: grayBg),
|
||||
popupMenuTheme: PopupMenuThemeData(
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: isDesktop ? Color(0xFFECECEC) : Colors.transparent),
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
)),
|
||||
).copyWith(
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ColorThemeExtension.light,
|
||||
@@ -383,6 +420,7 @@ class MyTheme {
|
||||
],
|
||||
);
|
||||
static ThemeData darkTheme = ThemeData(
|
||||
useMaterial3: false,
|
||||
brightness: Brightness.dark,
|
||||
hoverColor: Color.fromARGB(255, 45, 46, 53),
|
||||
scaffoldBackgroundColor: Color(0xFF18191E),
|
||||
@@ -474,6 +512,11 @@ class MyTheme {
|
||||
secondary: accent,
|
||||
background: Color(0xFF24252B),
|
||||
),
|
||||
popupMenuTheme: PopupMenuThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: Colors.white24),
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
)),
|
||||
).copyWith(
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ColorThemeExtension.dark,
|
||||
@@ -844,16 +887,16 @@ class OverlayDialogManager {
|
||||
}
|
||||
}
|
||||
|
||||
void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
||||
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
||||
final overlayState = globalKey.currentState?.overlay;
|
||||
if (overlayState == null) return;
|
||||
final entry = OverlayEntry(builder: (_) {
|
||||
final entry = OverlayEntry(builder: (context) {
|
||||
return IgnorePointer(
|
||||
child: Align(
|
||||
alignment: const Alignment(0.0, 0.8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
color: MyTheme.color(context).toastBg,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
@@ -862,11 +905,11 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 18,
|
||||
color: Colors.white),
|
||||
color: MyTheme.color(context).toastText),
|
||||
),
|
||||
)));
|
||||
});
|
||||
@@ -954,7 +997,7 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
|
||||
void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel, ReconnectHandle? reconnect}) {
|
||||
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
|
||||
dialogManager.dismissAll();
|
||||
List<Widget> buttons = [];
|
||||
bool hasOk = false;
|
||||
@@ -994,22 +1037,21 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
}
|
||||
if (reconnect != null && title == "Connection Error") {
|
||||
if (reconnect != null &&
|
||||
title == "Connection Error" &&
|
||||
reconnectTimeout != null) {
|
||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||
final enabled = true.obs;
|
||||
final button = Obx(
|
||||
() => dialogButton(
|
||||
'Reconnect',
|
||||
isOutline: true,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
final button = Obx(() => _ReconnectCountDownButton(
|
||||
second: reconnectTimeout,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
));
|
||||
buttons.insert(0, button);
|
||||
}
|
||||
if (link.isNotEmpty) {
|
||||
@@ -1054,7 +1096,7 @@ Widget msgboxIcon(String type) {
|
||||
if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
||||
iconData = Icons.admin_panel_settings;
|
||||
}
|
||||
if (type == "info") {
|
||||
if (type.contains('info')) {
|
||||
iconData = Icons.info;
|
||||
}
|
||||
if (iconData != null) {
|
||||
@@ -1490,8 +1532,8 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
late Offset position;
|
||||
late Size sz;
|
||||
late bool isMaximized;
|
||||
bool isFullscreen = stateGlobal.fullscreen ||
|
||||
(Platform.isMacOS && stateGlobal.closeOnFullscreen);
|
||||
bool isFullscreen = stateGlobal.fullscreen.isTrue ||
|
||||
(Platform.isMacOS && stateGlobal.closeOnFullscreen == true);
|
||||
setFrameIfMaximized() {
|
||||
if (isMaximized) {
|
||||
final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
|
||||
@@ -1669,8 +1711,10 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
||||
|
||||
/// Restore window position and size on start
|
||||
/// Note that windowId must be provided if it's subwindow
|
||||
//
|
||||
// display is used to set the offset of the window in individual display mode.
|
||||
Future<bool> restoreWindowPosition(WindowType type,
|
||||
{int? windowId, String? peerId}) async {
|
||||
{int? windowId, String? peerId, int? display}) async {
|
||||
if (bind
|
||||
.mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION")
|
||||
.isNotEmpty) {
|
||||
@@ -1706,14 +1750,22 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
debugPrint("no window position saved, ignoring position restoration");
|
||||
return false;
|
||||
}
|
||||
if (type == WindowType.RemoteDesktop &&
|
||||
!isRemotePeerPos &&
|
||||
windowId != null) {
|
||||
if (lpos.offsetWidth != null) {
|
||||
lpos.offsetWidth = lpos.offsetWidth! + windowId * 20;
|
||||
if (type == WindowType.RemoteDesktop) {
|
||||
if (!isRemotePeerPos && windowId != null) {
|
||||
if (lpos.offsetWidth != null) {
|
||||
lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset;
|
||||
}
|
||||
if (lpos.offsetHeight != null) {
|
||||
lpos.offsetHeight = lpos.offsetHeight! + windowId * kNewWindowOffset;
|
||||
}
|
||||
}
|
||||
if (lpos.offsetHeight != null) {
|
||||
lpos.offsetHeight = lpos.offsetHeight! + windowId * 20;
|
||||
if (display != null) {
|
||||
if (lpos.offsetWidth != null) {
|
||||
lpos.offsetWidth = lpos.offsetWidth! + display * kNewWindowOffset;
|
||||
}
|
||||
if (lpos.offsetHeight != null) {
|
||||
lpos.offsetHeight = lpos.offsetHeight! + display * kNewWindowOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1942,6 +1994,7 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
String? command;
|
||||
String? id;
|
||||
final options = ["connect", "play", "file-transfer", "port-forward", "rdp"];
|
||||
if (uri.authority.isEmpty &&
|
||||
uri.path.split('').every((char) => char == '/')) {
|
||||
return [];
|
||||
@@ -1949,16 +2002,65 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
// For compatibility
|
||||
command = '--connect';
|
||||
id = uri.path.substring("/new/".length);
|
||||
} else if (['connect', "play", 'file-transfer', 'port-forward', 'rdp']
|
||||
.contains(uri.authority)) {
|
||||
} else if (uri.authority == "config") {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final config = uri.path.substring("/".length);
|
||||
// add a timer to make showToast work
|
||||
Timer(Duration(seconds: 1), () {
|
||||
importConfig(null, null, config);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} else if (uri.authority == "password") {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final password = uri.path.substring("/".length);
|
||||
if (password.isNotEmpty) {
|
||||
Timer(Duration(seconds: 1), () async {
|
||||
await bind.mainSetPermanentPassword(password: password);
|
||||
showToast(translate('Successful'));
|
||||
});
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
} else if (uri.authority.length > 2 && uri.path.length <= 1) {
|
||||
if (isMobile && id != null) {
|
||||
if (optionIndex == 0 || optionIndex == 1) {
|
||||
connect(Get.context!, id);
|
||||
} else if (optionIndex == 2) {
|
||||
connect(Get.context!, id, isFileTransfer: true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else if (uri.authority.length > 2 &&
|
||||
(uri.path.length <= 1 ||
|
||||
(uri.path == '/r' || uri.path.startsWith('/r@')))) {
|
||||
// rustdesk://<connect-id>
|
||||
// rustdesk://<connect-id>/r
|
||||
// rustdesk://<connect-id>/r@<server>
|
||||
command = '--connect';
|
||||
id = uri.authority;
|
||||
if (uri.path.length > 1) {
|
||||
id = id + uri.path;
|
||||
}
|
||||
}
|
||||
|
||||
var key = uri.queryParameters["key"];
|
||||
if (id != null) {
|
||||
if (key != null) {
|
||||
id = "$id?key=$key";
|
||||
}
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
if (id != null) {
|
||||
final forceRelay = uri.queryParameters["relay"] != null;
|
||||
connect(Get.context!, id, forceRelay: forceRelay);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> args = List.empty(growable: true);
|
||||
@@ -2003,6 +2105,7 @@ connect(
|
||||
bool isFileTransfer = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false,
|
||||
}) async {
|
||||
if (id == '') return;
|
||||
if (!isDesktop || desktopType == DesktopType.main) {
|
||||
@@ -2011,12 +2114,16 @@ connect(
|
||||
final idController = Get.find<IDTextEditingController>();
|
||||
idController.text = formatID(id);
|
||||
}
|
||||
if (Get.isRegistered<TextEditingController>()) {
|
||||
final fieldTextEditingController = Get.find<TextEditingController>();
|
||||
fieldTextEditingController.text = formatID(id);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
id = id.replaceAll(' ', '');
|
||||
final oldId = id;
|
||||
id = await bind.mainHandleRelayId(id: id);
|
||||
final forceRelay = id != oldId;
|
||||
final forceRelay2 = id != oldId || forceRelay;
|
||||
assert(!(isFileTransfer && isTcpTunneling && isRDP),
|
||||
"more than one connect type");
|
||||
|
||||
@@ -2027,7 +2134,7 @@ connect(
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
forceRelay: forceRelay,
|
||||
forceRelay: forceRelay2,
|
||||
);
|
||||
} else {
|
||||
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
|
||||
@@ -2317,7 +2424,7 @@ Widget dialogButton(String text,
|
||||
}
|
||||
}
|
||||
|
||||
int version_cmp(String v1, String v2) {
|
||||
int versionCmp(String v1, String v2) {
|
||||
return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2);
|
||||
}
|
||||
|
||||
@@ -2448,7 +2555,7 @@ Future<void> start_service(bool is_start) async {
|
||||
}
|
||||
}
|
||||
|
||||
typedef Future<bool> WhetherUseRemoteBlock();
|
||||
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
||||
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
|
||||
var block = false.obs;
|
||||
return Obx(() => MouseRegion(
|
||||
@@ -2589,3 +2696,377 @@ String getDesktopTabLabel(String peerId, String alias) {
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
|
||||
if (pi.currentDisplay == kAllDisplayValue) {
|
||||
for (int i = 0; i < pi.displays.length; i++) {
|
||||
await bind.sessionRefresh(sessionId: sessionId, display: i);
|
||||
}
|
||||
} else {
|
||||
await bind.sessionRefresh(sessionId: sessionId, display: pi.currentDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
|
||||
pi.isSupportMultiDisplay &&
|
||||
useTextureRender &&
|
||||
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';
|
||||
|
||||
Future<List<Rect>> getScreenListWayland() async {
|
||||
final screenRectList = <Rect>[];
|
||||
if (isMainDesktopWindow) {
|
||||
for (var screen in await window_size.getScreenList()) {
|
||||
final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
|
||||
double l = screen.frame.left;
|
||||
double t = screen.frame.top;
|
||||
double r = screen.frame.right;
|
||||
double b = screen.frame.bottom;
|
||||
final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
|
||||
screenRectList.add(rect);
|
||||
}
|
||||
} else {
|
||||
final screenList = await rustDeskWinManager.call(
|
||||
WindowType.Main, kWindowGetScreenList, '');
|
||||
try {
|
||||
for (var screen in jsonDecode(screenList.result) as List<dynamic>) {
|
||||
final scale = kIgnoreDpi ? 1.0 : screen['scaleFactor'];
|
||||
double l = screen['frame']['l'];
|
||||
double t = screen['frame']['t'];
|
||||
double r = screen['frame']['r'];
|
||||
double b = screen['frame']['b'];
|
||||
final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
|
||||
screenRectList.add(rect);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to parse screenList: $e');
|
||||
}
|
||||
}
|
||||
return screenRectList;
|
||||
}
|
||||
|
||||
Future<List<Rect>> getScreenListNotWayland() async {
|
||||
final screenRectList = <Rect>[];
|
||||
final displays = bind.mainGetDisplays();
|
||||
if (displays.isEmpty) {
|
||||
return screenRectList;
|
||||
}
|
||||
try {
|
||||
for (var display in jsonDecode(displays) as List<dynamic>) {
|
||||
// to-do: scale factor ?
|
||||
// final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
|
||||
double l = display['x'].toDouble();
|
||||
double t = display['y'].toDouble();
|
||||
double r = (display['x'] + display['w']).toDouble();
|
||||
double b = (display['y'] + display['h']).toDouble();
|
||||
screenRectList.add(Rect.fromLTRB(l, t, r, b));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to parse displays: $e');
|
||||
}
|
||||
return screenRectList;
|
||||
}
|
||||
|
||||
Future<List<Rect>> getScreenRectList() async {
|
||||
return bind.mainCurrentIsWayland()
|
||||
? await getScreenListWayland()
|
||||
: await getScreenListNotWayland();
|
||||
}
|
||||
|
||||
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
||||
{bool updateCursorPos = true}) {
|
||||
final displays = i == kAllDisplayValue
|
||||
? List.generate(pi.displays.length, (index) => index)
|
||||
: [i];
|
||||
bind.sessionSwitchDisplay(
|
||||
isDesktop: isDesktop,
|
||||
sessionId: ffi.sessionId,
|
||||
value: Int32List.fromList(displays),
|
||||
);
|
||||
ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, ffi.id,
|
||||
updateCursorPos: updateCursorPos);
|
||||
}
|
||||
|
||||
// Open new tab or window to show this monitor.
|
||||
// For now just open new window.
|
||||
//
|
||||
// screenRect is used to move the new window to the specified screen and set fullscreen.
|
||||
openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
||||
{Rect? screenRect}) {
|
||||
final args = {
|
||||
'window_id': stateGlobal.windowId,
|
||||
'peer_id': peerId,
|
||||
'display': i,
|
||||
'display_count': pi.displays.length,
|
||||
};
|
||||
if (screenRect != null) {
|
||||
args['screen_rect'] = {
|
||||
'l': screenRect.left,
|
||||
't': screenRect.top,
|
||||
'r': screenRect.right,
|
||||
'b': screenRect.bottom,
|
||||
};
|
||||
}
|
||||
DesktopMultiWindow.invokeMethod(
|
||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||
}
|
||||
|
||||
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
|
||||
if (screenRect == null) {
|
||||
return;
|
||||
}
|
||||
final wc = WindowController.fromWindowId(stateGlobal.windowId);
|
||||
final curFrame = await wc.getFrame();
|
||||
final frame =
|
||||
Rect.fromLTWH(screenRect.left + 30, screenRect.top + 30, 600, 400);
|
||||
if (stateGlobal.fullscreen.isTrue &&
|
||||
curFrame.left <= frame.left &&
|
||||
curFrame.top <= frame.top &&
|
||||
curFrame.width >= frame.width &&
|
||||
curFrame.height >= frame.height) {
|
||||
return;
|
||||
}
|
||||
await wc.setFrame(frame);
|
||||
// An duration is needed to avoid the window being restored after fullscreen.
|
||||
Future.delayed(Duration(milliseconds: 300), () async {
|
||||
stateGlobal.setFullscreen(true);
|
||||
});
|
||||
}
|
||||
|
||||
parseParamScreenRect(Map<String, dynamic> params) {
|
||||
Rect? screenRect;
|
||||
if (params['screen_rect'] != null) {
|
||||
double l = params['screen_rect']['l'];
|
||||
double t = params['screen_rect']['t'];
|
||||
double r = params['screen_rect']['r'];
|
||||
double b = params['screen_rect']['b'];
|
||||
screenRect = Rect.fromLTRB(l, t, r, b);
|
||||
}
|
||||
return screenRect;
|
||||
}
|
||||
|
||||
get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2";
|
||||
|
||||
class _ReconnectCountDownButton extends StatefulWidget {
|
||||
_ReconnectCountDownButton({
|
||||
Key? key,
|
||||
required this.second,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
final VoidCallback? onPressed;
|
||||
final int second;
|
||||
|
||||
@override
|
||||
State<_ReconnectCountDownButton> createState() =>
|
||||
_ReconnectCountDownButtonState();
|
||||
}
|
||||
|
||||
class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
|
||||
late int _countdownSeconds = widget.second;
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startCountdownTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startCountdownTimer() {
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (_countdownSeconds <= 0) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
setState(() {
|
||||
_countdownSeconds--;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return dialogButton(
|
||||
'${translate('Reconnect')} (${_countdownSeconds}s)',
|
||||
onPressed: widget.onPressed,
|
||||
isOutline: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
||||
String? text) {
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
final sc = ServerConfig.decode(text);
|
||||
if (sc.idServer.isNotEmpty) {
|
||||
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
||||
success.then((value) {
|
||||
if (value) {
|
||||
showToast(translate('Import server configuration successfully'));
|
||||
} else {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
return sc;
|
||||
} catch (e) {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
} else {
|
||||
showToast(translate('Clipboard is empty'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> setServerConfig(
|
||||
List<TextEditingController>? controllers,
|
||||
List<RxString>? errMsgs,
|
||||
ServerConfig config,
|
||||
) async {
|
||||
config.idServer = config.idServer.trim();
|
||||
config.relayServer = config.relayServer.trim();
|
||||
config.apiServer = config.apiServer.trim();
|
||||
config.key = config.key.trim();
|
||||
if (controllers != null) {
|
||||
controllers[0].text = config.idServer;
|
||||
controllers[1].text = config.relayServer;
|
||||
controllers[2].text = config.apiServer;
|
||||
controllers[3].text = config.key;
|
||||
}
|
||||
// id
|
||||
if (config.idServer.isNotEmpty && errMsgs != null) {
|
||||
errMsgs[0].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.idServer));
|
||||
if (errMsgs[0].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// relay
|
||||
if (config.relayServer.isNotEmpty && errMsgs != null) {
|
||||
errMsgs[1].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.relayServer));
|
||||
if (errMsgs[1].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// api
|
||||
if (config.apiServer.isNotEmpty && errMsgs != null) {
|
||||
if (!config.apiServer.startsWith('http://') &&
|
||||
!config.apiServer.startsWith('https://')) {
|
||||
errMsgs[2].value =
|
||||
'${translate("API Server")}: ${translate("invalid_http")}';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final oldApiServer = await bind.mainGetApiServer();
|
||||
|
||||
// should set one by one
|
||||
await bind.mainSetOption(
|
||||
key: 'custom-rendezvous-server', value: config.idServer);
|
||||
await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
|
||||
await bind.mainSetOption(key: 'api-server', value: config.apiServer);
|
||||
await bind.mainSetOption(key: 'key', value: config.key);
|
||||
|
||||
final newApiServer = await bind.mainGetApiServer();
|
||||
if (oldApiServer.isNotEmpty &&
|
||||
oldApiServer != newApiServer &&
|
||||
gFFI.userModel.isLogin) {
|
||||
gFFI.userModel.logOut(apiServer: oldApiServer);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ColorFilter? svgColor(Color? color) {
|
||||
if (color == null) {
|
||||
return null;
|
||||
} else {
|
||||
return ColorFilter.mode(color, BlendMode.srcIn);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class ComboBox extends StatelessWidget {
|
||||
late final List<String> keys;
|
||||
late final List<String> values;
|
||||
late final String initialKey;
|
||||
late final Function(String key) onChanged;
|
||||
late final bool enabled;
|
||||
late String current;
|
||||
|
||||
ComboBox({
|
||||
Key? key,
|
||||
required this.keys,
|
||||
required this.values,
|
||||
required this.initialKey,
|
||||
required this.onChanged,
|
||||
this.enabled = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var index = keys.indexOf(initialKey);
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
var ref = values[index].obs;
|
||||
current = keys[index];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: enabled
|
||||
? MyTheme.color(context).border2 ?? MyTheme.border
|
||||
: MyTheme.border,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8), //border raiuds of dropdown button
|
||||
),
|
||||
height: 42, // should be the height of a TextField
|
||||
child: Obx(() => DropdownButton<String>(
|
||||
isExpanded: true,
|
||||
value: ref.value,
|
||||
elevation: 16,
|
||||
underline: Container(),
|
||||
style: TextStyle(
|
||||
color: enabled
|
||||
? Theme.of(context).textTheme.titleMedium?.color
|
||||
: disabledTextColor(context, enabled)),
|
||||
icon: const Icon(
|
||||
Icons.expand_more_sharp,
|
||||
size: 20,
|
||||
).marginOnly(right: 15),
|
||||
onChanged: enabled
|
||||
? (String? newValue) {
|
||||
if (newValue != null && newValue != ref.value) {
|
||||
ref.value = newValue;
|
||||
current = newValue;
|
||||
onChanged(keys[values.indexOf(newValue)]);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
items: values.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).marginOnly(left: 15),
|
||||
);
|
||||
}).toList(),
|
||||
)),
|
||||
).marginOnly(bottom: 5);
|
||||
}
|
||||
}
|
||||
|
||||
Color? disabledTextColor(BuildContext context, bool enabled) {
|
||||
return enabled
|
||||
? null
|
||||
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ class HttpType {
|
||||
static const kAuthReqTypeMobile = "mobile";
|
||||
static const kAuthReqTypeSMSCode = "sms_code";
|
||||
static const kAuthReqTypeEmailCode = "email_code";
|
||||
static const kAuthReqTypeTfaCode = "tfa_code";
|
||||
|
||||
static const kAuthResTypeToken = "access_token";
|
||||
static const kAuthResTypeEmailCheck = "email_check";
|
||||
static const kAuthResTypeTfaCheck = "tfa_check";
|
||||
}
|
||||
|
||||
enum UserStatus { kDisabled, kNormal, kUnverified }
|
||||
@@ -118,6 +120,8 @@ class LoginRequest {
|
||||
bool? autoLogin;
|
||||
String? type;
|
||||
String? verificationCode;
|
||||
String? tfaCode;
|
||||
String? secret;
|
||||
|
||||
LoginRequest(
|
||||
{this.username,
|
||||
@@ -126,7 +130,9 @@ class LoginRequest {
|
||||
this.uuid,
|
||||
this.autoLogin,
|
||||
this.type,
|
||||
this.verificationCode});
|
||||
this.verificationCode,
|
||||
this.tfaCode,
|
||||
this.secret});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
@@ -139,6 +145,8 @@ class LoginRequest {
|
||||
if (verificationCode != null) {
|
||||
data['verificationCode'] = verificationCode;
|
||||
}
|
||||
if (tfaCode != null) data['tfaCode'] = tfaCode;
|
||||
if (secret != null) data['secret'] = secret;
|
||||
|
||||
Map<String, dynamic> deviceInfo = {};
|
||||
try {
|
||||
@@ -154,13 +162,18 @@ class LoginRequest {
|
||||
class LoginResponse {
|
||||
String? access_token;
|
||||
String? type;
|
||||
String? tfa_type;
|
||||
String? secret;
|
||||
UserPayload? user;
|
||||
|
||||
LoginResponse({this.access_token, this.type, this.user});
|
||||
LoginResponse(
|
||||
{this.access_token, this.type, this.tfa_type, this.secret, this.user});
|
||||
|
||||
LoginResponse.fromJson(Map<String, dynamic> json) {
|
||||
access_token = json['access_token'];
|
||||
type = json['type'];
|
||||
tfa_type = json['tfa_type'];
|
||||
secret = json['secret'];
|
||||
user = json['user'] != null ? UserPayload.fromJson(json['user']) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../consts.dart';
|
||||
@@ -10,7 +11,7 @@ class PrivacyModeState {
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
final RxString state = ''.obs;
|
||||
Get.put(state, tag: key);
|
||||
}
|
||||
}
|
||||
@@ -20,11 +21,11 @@ class PrivacyModeState {
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
Get.find<RxString>(tag: key).value = '';
|
||||
}
|
||||
}
|
||||
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
static RxString find(String id) => Get.find<RxString>(tag: tag(id));
|
||||
}
|
||||
|
||||
class BlockInputState {
|
||||
@@ -318,6 +319,7 @@ initSharedStates(String id) {
|
||||
FingerprintState.init(id);
|
||||
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
||||
UnreadChatCountState.init(id);
|
||||
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
||||
}
|
||||
|
||||
removeSharedStates(String id) {
|
||||
@@ -330,4 +332,5 @@ removeSharedStates(String id) {
|
||||
FingerprintState.delete(id);
|
||||
PeerBoolOption.delete(id, 'zoom-cursor');
|
||||
UnreadChatCountState.delete(id);
|
||||
if (isMobile) ConnectionTypeState.delete(id);
|
||||
}
|
||||
|
||||
205
flutter/lib/common/widgets/autocomplete.dart
Normal file
205
flutter/lib/common/widgets/autocomplete.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import '../../../models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||
|
||||
Future<List<Peer>> getAllPeers() async {
|
||||
Map<String, dynamic> recentPeers = jsonDecode(bind.mainLoadRecentPeersSync());
|
||||
Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
|
||||
Map<String, dynamic> abPeers = jsonDecode(bind.mainLoadAbSync());
|
||||
Map<String, dynamic> groupPeers = jsonDecode(bind.mainLoadGroupSync());
|
||||
|
||||
Map<String, dynamic> combinedPeers = {};
|
||||
|
||||
void mergePeers(Map<String, dynamic> peers) {
|
||||
if (peers.containsKey("peers")) {
|
||||
dynamic peerData = peers["peers"];
|
||||
|
||||
if (peerData is String) {
|
||||
try {
|
||||
peerData = jsonDecode(peerData);
|
||||
} catch (e) {
|
||||
print("Error decoding peers: $e");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (peerData is List) {
|
||||
for (var peer in peerData) {
|
||||
if (peer is Map && peer.containsKey("id")) {
|
||||
String id = peer["id"];
|
||||
if (!combinedPeers.containsKey(id)) {
|
||||
combinedPeers[id] = peer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mergePeers(recentPeers);
|
||||
mergePeers(lanPeers);
|
||||
mergePeers(abPeers);
|
||||
mergePeers(groupPeers);
|
||||
|
||||
List<Peer> parsedPeers = [];
|
||||
|
||||
for (var peer in combinedPeers.values) {
|
||||
parsedPeers.add(Peer.fromJson(peer));
|
||||
}
|
||||
return parsedPeers;
|
||||
}
|
||||
|
||||
class AutocompletePeerTile extends StatefulWidget {
|
||||
final VoidCallback onSelect;
|
||||
final Peer peer;
|
||||
|
||||
const AutocompletePeerTile({
|
||||
Key? key,
|
||||
required this.onSelect,
|
||||
required this.peer,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
AutocompletePeerTileState createState() => AutocompletePeerTileState();
|
||||
}
|
||||
|
||||
class AutocompletePeerTileState extends State<AutocompletePeerTile> {
|
||||
List _frontN<T>(List list, int n) {
|
||||
if (list.length <= n) {
|
||||
return list;
|
||||
} else {
|
||||
return list.sublist(0, n);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double tileRadius = 5;
|
||||
final name =
|
||||
'${widget.peer.username}${widget.peer.username.isNotEmpty && widget.peer.hostname.isNotEmpty ? '@' : ''}${widget.peer.hostname}';
|
||||
final greyStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||
final child = GestureDetector(
|
||||
onTap: () => widget.onSelect(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 5, right: 5),
|
||||
child: Container(
|
||||
height: 42,
|
||||
margin: EdgeInsets.only(bottom: 5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: str2color(
|
||||
'${widget.peer.id}${widget.peer.platform}', 0x7f),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(tileRadius),
|
||||
bottomLeft: Radius.circular(tileRadius),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
width: 42,
|
||||
height: null,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: getPlatformImage(widget.peer.platform,
|
||||
size: 30))),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(tileRadius),
|
||||
bottomRight: Radius.circular(tileRadius),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 2),
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 2),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin:
|
||||
EdgeInsets.only(top: 2),
|
||||
child: Row(children: [
|
||||
getOnline(
|
||||
8, widget.peer.online),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.peer.alias.isEmpty
|
||||
? formatID(
|
||||
widget.peer.id)
|
||||
: widget.peer.alias,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall,
|
||||
)),
|
||||
widget.peer.alias.isNotEmpty
|
||||
? Padding(
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.only(
|
||||
left: 5,
|
||||
right: 5),
|
||||
child: Text(
|
||||
"(${widget.peer.id})",
|
||||
style: greyStyle,
|
||||
overflow:
|
||||
TextOverflow
|
||||
.ellipsis,
|
||||
))
|
||||
: Container(),
|
||||
])),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
)))),
|
||||
],
|
||||
)),
|
||||
)
|
||||
],
|
||||
))));
|
||||
final colors = _frontN(widget.peer.tags, 25)
|
||||
.map((e) => gFFI.abModel.getTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
? ''
|
||||
: widget.peer.tags.isNotEmpty
|
||||
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
|
||||
: '',
|
||||
child: Stack(children: [
|
||||
child,
|
||||
if (colors.isNotEmpty)
|
||||
Positioned(
|
||||
top: 5,
|
||||
right: 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
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:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/model.dart';
|
||||
@@ -359,6 +363,8 @@ class DialogTextField extends StatelessWidget {
|
||||
final Widget? suffixIcon;
|
||||
final TextEditingController controller;
|
||||
final FocusNode? focusNode;
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
static const kUsernameTitle = 'Username';
|
||||
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
||||
@@ -374,6 +380,8 @@ class DialogTextField extends StatelessWidget {
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.hintText,
|
||||
this.keyboardType,
|
||||
this.inputFormatters,
|
||||
required this.title,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
@@ -398,6 +406,8 @@ class DialogTextField extends StatelessWidget {
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -405,17 +415,260 @@ class DialogTextField extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ValidationField extends StatelessWidget {
|
||||
ValidationField({Key? key}) : super(key: key);
|
||||
|
||||
String? validate();
|
||||
bool get isReady;
|
||||
}
|
||||
|
||||
class Dialog2FaField extends ValidationField {
|
||||
Dialog2FaField({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
this.autoFocus = true,
|
||||
this.reRequestFocus = false,
|
||||
this.title,
|
||||
this.hintText,
|
||||
this.errorText,
|
||||
this.readyCallback,
|
||||
this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
final bool autoFocus;
|
||||
final bool reRequestFocus;
|
||||
final String? title;
|
||||
final String? hintText;
|
||||
final String? errorText;
|
||||
final VoidCallback? readyCallback;
|
||||
final VoidCallback? onChanged;
|
||||
final errMsg = translate('2FA code must be 6 digits.');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DialogVerificationCodeField(
|
||||
title: title ?? translate('2FA code'),
|
||||
controller: controller,
|
||||
errorText: errorText,
|
||||
autoFocus: autoFocus,
|
||||
reRequestFocus: reRequestFocus,
|
||||
hintText: hintText,
|
||||
readyCallback: readyCallback,
|
||||
onChanged: _onChanged,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String get text => controller.text;
|
||||
bool get isAllDigits => text.codeUnits.every((e) => e >= 48 && e <= 57);
|
||||
|
||||
@override
|
||||
bool get isReady => text.length == 6 && isAllDigits;
|
||||
|
||||
@override
|
||||
String? validate() => isReady ? null : errMsg;
|
||||
|
||||
_onChanged(StateSetter setState, SimpleWrapper<String?> errText) {
|
||||
onChanged?.call();
|
||||
|
||||
if (text.length > 6) {
|
||||
setState(() => errText.value = errMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAllDigits) {
|
||||
setState(() => errText.value = errMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isReady) {
|
||||
readyCallback?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
if (errText.value != null) {
|
||||
setState(() => errText.value = null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DialogEmailCodeField extends ValidationField {
|
||||
DialogEmailCodeField({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
this.autoFocus = true,
|
||||
this.reRequestFocus = false,
|
||||
this.hintText,
|
||||
this.errorText,
|
||||
this.readyCallback,
|
||||
this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
final bool autoFocus;
|
||||
final bool reRequestFocus;
|
||||
final String? hintText;
|
||||
final String? errorText;
|
||||
final VoidCallback? readyCallback;
|
||||
final VoidCallback? onChanged;
|
||||
final errMsg = translate('Email verification code must be 6 characters.');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DialogVerificationCodeField(
|
||||
title: translate('Verification code'),
|
||||
controller: controller,
|
||||
errorText: errorText,
|
||||
autoFocus: autoFocus,
|
||||
reRequestFocus: reRequestFocus,
|
||||
hintText: hintText,
|
||||
readyCallback: readyCallback,
|
||||
helperText: translate('verification_tip'),
|
||||
onChanged: _onChanged,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
);
|
||||
}
|
||||
|
||||
String get text => controller.text;
|
||||
|
||||
@override
|
||||
bool get isReady => text.length == 6;
|
||||
|
||||
@override
|
||||
String? validate() => isReady ? null : errMsg;
|
||||
|
||||
_onChanged(StateSetter setState, SimpleWrapper<String?> errText) {
|
||||
onChanged?.call();
|
||||
|
||||
if (text.length > 6) {
|
||||
setState(() => errText.value = errMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isReady) {
|
||||
readyCallback?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
if (errText.value != null) {
|
||||
setState(() => errText.value = null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DialogVerificationCodeField extends StatefulWidget {
|
||||
DialogVerificationCodeField({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required this.title,
|
||||
this.autoFocus = true,
|
||||
this.reRequestFocus = false,
|
||||
this.helperText,
|
||||
this.hintText,
|
||||
this.errorText,
|
||||
this.textLength,
|
||||
this.readyCallback,
|
||||
this.onChanged,
|
||||
this.keyboardType,
|
||||
this.inputFormatters,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
final bool autoFocus;
|
||||
final bool reRequestFocus;
|
||||
final String title;
|
||||
final String? helperText;
|
||||
final String? hintText;
|
||||
final String? errorText;
|
||||
final int? textLength;
|
||||
final VoidCallback? readyCallback;
|
||||
final Function(StateSetter setState, SimpleWrapper<String?> errText)?
|
||||
onChanged;
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
@override
|
||||
State<DialogVerificationCodeField> createState() =>
|
||||
_DialogVerificationCodeField();
|
||||
}
|
||||
|
||||
class _DialogVerificationCodeField extends State<DialogVerificationCodeField> {
|
||||
final _focusNode = FocusNode();
|
||||
Timer? _timer;
|
||||
Timer? _timerReRequestFocus;
|
||||
SimpleWrapper<String?> errorText = SimpleWrapper(null);
|
||||
String _preText = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.autoFocus) {
|
||||
_timer =
|
||||
Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
|
||||
|
||||
if (widget.onChanged != null) {
|
||||
widget.controller.addListener(() {
|
||||
final text = widget.controller.text.trim();
|
||||
if (text == _preText) return;
|
||||
widget.onChanged!(setState, errorText);
|
||||
_preText = text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// software secure keyboard will take the focus since flutter 3.13
|
||||
// request focus again when android account password obtain focus
|
||||
if (Platform.isAndroid && widget.reRequestFocus) {
|
||||
_focusNode.addListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_timerReRequestFocus?.cancel();
|
||||
_timerReRequestFocus = Timer(
|
||||
Duration(milliseconds: 100), () => _focusNode.requestFocus());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timerReRequestFocus?.cancel();
|
||||
_focusNode.unfocus();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DialogTextField(
|
||||
title: widget.title,
|
||||
controller: widget.controller,
|
||||
errorText: widget.errorText ?? errorText.value,
|
||||
focusNode: _focusNode,
|
||||
helperText: widget.helperText,
|
||||
keyboardType: widget.keyboardType,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordWidget extends StatefulWidget {
|
||||
PasswordWidget({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
this.autoFocus = true,
|
||||
this.reRequestFocus = false,
|
||||
this.hintText,
|
||||
this.errorText,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
final bool autoFocus;
|
||||
final bool reRequestFocus;
|
||||
final String? hintText;
|
||||
final String? errorText;
|
||||
|
||||
@@ -427,6 +680,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
bool _passwordVisible = false;
|
||||
final _focusNode = FocusNode();
|
||||
Timer? _timer;
|
||||
Timer? _timerReRequestFocus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -435,11 +689,23 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
_timer =
|
||||
Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
|
||||
}
|
||||
// software secure keyboard will take the focus since flutter 3.13
|
||||
// request focus again when android account password obtain focus
|
||||
if (Platform.isAndroid && widget.reRequestFocus) {
|
||||
_focusNode.addListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_timerReRequestFocus?.cancel();
|
||||
_timerReRequestFocus = Timer(
|
||||
Duration(milliseconds: 100), () => _focusNode.requestFocus());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timerReRequestFocus?.cancel();
|
||||
_focusNode.unfocus();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
@@ -972,7 +1238,7 @@ void showRestartRemoteDevice(PeerInfo pi, String id, SessionID sessionId,
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
|
||||
Flexible(
|
||||
child: Text(translate("Restart Remote Device"))
|
||||
child: Text(translate("Restart remote device"))
|
||||
.paddingOnly(left: 10)),
|
||||
]),
|
||||
content: Text(
|
||||
@@ -1244,11 +1510,24 @@ void showConfirmSwitchSidesDialog(
|
||||
}
|
||||
|
||||
customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
double qualityInitValue = 50;
|
||||
double fpsInitValue = 30;
|
||||
double initQuality = kDefaultQuality;
|
||||
double initFps = kDefaultFps;
|
||||
bool qualitySet = false;
|
||||
bool fpsSet = false;
|
||||
|
||||
bool? direct;
|
||||
try {
|
||||
direct =
|
||||
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
|
||||
} catch (_) {}
|
||||
bool hideFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
|
||||
bool hideMoreQuality =
|
||||
(await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
versionCmp(ffi.ffiModel.pi.version, '1.2.2') < 0;
|
||||
|
||||
setCustomValues({double? quality, double? fps}) async {
|
||||
debugPrint("setCustomValues quality:$quality, fps:$fps");
|
||||
if (quality != null) {
|
||||
qualitySet = true;
|
||||
await bind.sessionSetCustomImageQuality(
|
||||
@@ -1261,12 +1540,12 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
if (!qualitySet) {
|
||||
qualitySet = true;
|
||||
await bind.sessionSetCustomImageQuality(
|
||||
sessionId: sessionId, value: qualityInitValue.toInt());
|
||||
sessionId: sessionId, value: initQuality.toInt());
|
||||
}
|
||||
if (!fpsSet) {
|
||||
if (!hideFps && !fpsSet) {
|
||||
fpsSet = true;
|
||||
await bind.sessionSetCustomFps(
|
||||
sessionId: sessionId, fps: fpsInitValue.toInt());
|
||||
sessionId: sessionId, fps: initFps.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1277,32 +1556,30 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
|
||||
// quality
|
||||
final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
|
||||
qualityInitValue =
|
||||
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
|
||||
if (qualityInitValue < 10 || qualityInitValue > 2000) {
|
||||
qualityInitValue = 50;
|
||||
initQuality = quality != null && quality.isNotEmpty
|
||||
? quality[0].toDouble()
|
||||
: kDefaultQuality;
|
||||
if (initQuality < kMinQuality ||
|
||||
initQuality > (!hideMoreQuality ? kMaxMoreQuality : kMaxQuality)) {
|
||||
initQuality = kDefaultQuality;
|
||||
}
|
||||
// fps
|
||||
final fpsOption =
|
||||
await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps');
|
||||
fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30;
|
||||
if (fpsInitValue < 5 || fpsInitValue > 120) {
|
||||
fpsInitValue = 30;
|
||||
initFps = fpsOption == null
|
||||
? kDefaultFps
|
||||
: double.tryParse(fpsOption) ?? kDefaultFps;
|
||||
if (initFps < kMinFps || initFps > kMaxFps) {
|
||||
initFps = kDefaultFps;
|
||||
}
|
||||
bool? direct;
|
||||
try {
|
||||
direct =
|
||||
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
|
||||
} catch (_) {}
|
||||
bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
|
||||
|
||||
final content = customImageQualityWidget(
|
||||
initQuality: qualityInitValue,
|
||||
initFps: fpsInitValue,
|
||||
initQuality: initQuality,
|
||||
initFps: initFps,
|
||||
setQuality: (v) => setCustomValues(quality: v),
|
||||
setFps: (v) => setCustomValues(fps: v),
|
||||
showFps: !notShowFps);
|
||||
showFps: !hideFps,
|
||||
showMoreQuality: !hideMoreQuality);
|
||||
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||
}
|
||||
|
||||
@@ -1473,3 +1750,162 @@ void renameDialog(
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void change2fa({Function()? callback}) async {
|
||||
if (bind.mainHasValid2FaSync()) {
|
||||
await bind.mainSetOption(key: "2fa", value: "");
|
||||
callback?.call();
|
||||
return;
|
||||
}
|
||||
var new2fa = (await bind.mainGenerate2Fa());
|
||||
final secretRegex = RegExp(r'secret=([^&]+)');
|
||||
final secret = secretRegex.firstMatch(new2fa)?.group(1);
|
||||
String? errorText;
|
||||
final controller = TextEditingController();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
onVerify() async {
|
||||
if (await bind.mainVerify2Fa(code: controller.text.trim())) {
|
||||
callback?.call();
|
||||
close();
|
||||
} else {
|
||||
errorText = translate('wrong-2fa-code');
|
||||
}
|
||||
}
|
||||
|
||||
final codeField = Dialog2FaField(
|
||||
controller: controller,
|
||||
errorText: errorText,
|
||||
onChanged: () => setState(() => errorText = null),
|
||||
title: translate('Verification code'),
|
||||
readyCallback: () {
|
||||
onVerify();
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
|
||||
getOnSubmit() => codeField.isReady ? onVerify : null;
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("enable-2fa-title")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(translate("enable-2fa-desc"),
|
||||
style: TextStyle(fontSize: 12))
|
||||
.marginOnly(bottom: 12),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: QrImageView(
|
||||
backgroundColor: Colors.white,
|
||||
data: new2fa,
|
||||
version: QrVersions.auto,
|
||||
size: 160,
|
||||
gapless: false,
|
||||
)).marginOnly(bottom: 6),
|
||||
SelectableText(secret ?? '', style: TextStyle(fontSize: 12))
|
||||
.marginOnly(bottom: 12),
|
||||
Row(children: [Expanded(child: codeField)]),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: getOnSubmit()),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void enter2FaDialog(
|
||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
final RxBool submitReady = false.obs;
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close, context) {
|
||||
cancel() {
|
||||
close();
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
submit() {
|
||||
gFFI.send2FA(sessionId, controller.text.trim());
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
}
|
||||
|
||||
late Dialog2FaField codeField;
|
||||
|
||||
codeField = Dialog2FaField(
|
||||
controller: controller,
|
||||
title: translate('Verification code'),
|
||||
onChanged: () => submitReady.value = codeField.isReady,
|
||||
);
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('enter-2fa-title')),
|
||||
content: codeField,
|
||||
actions: [
|
||||
dialogButton('Cancel',
|
||||
onPressed: cancel,
|
||||
isOutline: true,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color)),
|
||||
Obx(() => dialogButton(
|
||||
'OK',
|
||||
onPressed: submitReady.isTrue ? submit : null,
|
||||
)),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel);
|
||||
});
|
||||
}
|
||||
|
||||
// This dialog should not be dismissed, otherwise it will be black screen, have not reproduced this.
|
||||
void showWindowsSessionsDialog(
|
||||
String type,
|
||||
String title,
|
||||
String text,
|
||||
OverlayDialogManager dialogManager,
|
||||
SessionID sessionId,
|
||||
String peerId,
|
||||
String sessions) {
|
||||
List<dynamic> sessionsList = [];
|
||||
try {
|
||||
sessionsList = json.decode(sessions);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
List<String> sids = [];
|
||||
List<String> names = [];
|
||||
for (var session in sessionsList) {
|
||||
sids.add(session['sid']);
|
||||
names.add(session['name']);
|
||||
}
|
||||
String selectedUserValue = sids.first;
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
bind.sessionSendSelectedSessionId(
|
||||
sessionId: sessionId, sid: selectedUserValue);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
actions: [
|
||||
ComboBox(
|
||||
keys: sids,
|
||||
values: names,
|
||||
initialKey: selectedUserValue,
|
||||
onChanged: (value) {
|
||||
selectedUserValue = value;
|
||||
}),
|
||||
dialogButton('Connect', onPressed: submit, isOutline: false),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -357,6 +357,7 @@ class LoginWidgetUserPass extends StatelessWidget {
|
||||
PasswordWidget(
|
||||
controller: pass,
|
||||
autoFocus: false,
|
||||
reRequestFocus: true,
|
||||
errorText: passMsg,
|
||||
),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
@@ -389,8 +390,7 @@ class LoginWidgetUserPass extends StatelessWidget {
|
||||
|
||||
const kAuthReqTypeOidc = 'oidc/';
|
||||
|
||||
/// common login dialog for desktop
|
||||
/// call this directly
|
||||
// call this directly
|
||||
Future<bool?> loginDialog() async {
|
||||
var username =
|
||||
TextEditingController(text: UserModel.getLocalUserInfo()?['name'] ?? '');
|
||||
@@ -426,6 +426,55 @@ Future<bool?> loginDialog() async {
|
||||
close(false);
|
||||
}
|
||||
|
||||
handleLoginResponse(LoginResponse resp, bool storeIfAccessToken,
|
||||
void Function([dynamic])? close) async {
|
||||
switch (resp.type) {
|
||||
case HttpType.kAuthResTypeToken:
|
||||
if (resp.access_token != null) {
|
||||
if (storeIfAccessToken) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: 'access_token', value: resp.access_token!);
|
||||
await bind.mainSetLocalOption(
|
||||
key: 'user_info', value: jsonEncode(resp.user ?? {}));
|
||||
}
|
||||
if (close != null) {
|
||||
close(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case HttpType.kAuthResTypeEmailCheck:
|
||||
bool? isEmailVerification;
|
||||
if (resp.tfa_type == null ||
|
||||
resp.tfa_type == HttpType.kAuthResTypeEmailCheck) {
|
||||
isEmailVerification = true;
|
||||
} else if (resp.tfa_type == HttpType.kAuthResTypeTfaCheck) {
|
||||
isEmailVerification = false;
|
||||
} else {
|
||||
passwordMsg = "Failed, bad tfa type from server";
|
||||
}
|
||||
if (isEmailVerification != null) {
|
||||
if (isMobile) {
|
||||
if (close != null) close(false);
|
||||
verificationCodeDialog(
|
||||
resp.user, resp.secret, isEmailVerification);
|
||||
} else {
|
||||
setState(() => isInProgress = false);
|
||||
final res = await verificationCodeDialog(
|
||||
resp.user, resp.secret, isEmailVerification);
|
||||
if (res == true) {
|
||||
if (close != null) close(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
passwordMsg = "Failed, bad response from server";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onLogin() async {
|
||||
// validate
|
||||
if (username.text.isEmpty) {
|
||||
@@ -446,35 +495,7 @@ Future<bool?> loginDialog() async {
|
||||
uuid: await bind.mainGetUuid(),
|
||||
autoLogin: true,
|
||||
type: HttpType.kAuthReqTypeAccount));
|
||||
|
||||
switch (resp.type) {
|
||||
case HttpType.kAuthResTypeToken:
|
||||
if (resp.access_token != null) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: 'access_token', value: resp.access_token!);
|
||||
await bind.mainSetLocalOption(
|
||||
key: 'user_info', value: jsonEncode(resp.user ?? {}));
|
||||
close(true);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case HttpType.kAuthResTypeEmailCheck:
|
||||
if (isMobile) {
|
||||
close(true);
|
||||
verificationCodeDialog(resp.user);
|
||||
} else {
|
||||
setState(() => isInProgress = false);
|
||||
final res = await verificationCodeDialog(resp.user);
|
||||
if (res == true) {
|
||||
close(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
passwordMsg = "Failed, bad response from server";
|
||||
break;
|
||||
}
|
||||
await handleLoginResponse(resp, true, close);
|
||||
} on RequestException catch (err) {
|
||||
passwordMsg = translate(err.cause);
|
||||
} catch (err) {
|
||||
@@ -505,15 +526,21 @@ Future<bool?> loginDialog() async {
|
||||
.map((e) => ConfigOP(op: e['name'], icon: e['icon']))
|
||||
.toList(),
|
||||
curOP: curOP,
|
||||
cbLogin: (Map<String, dynamic> authBody) {
|
||||
cbLogin: (Map<String, dynamic> authBody) async {
|
||||
LoginResponse? resp;
|
||||
try {
|
||||
// access_token is already stored in the rust side.
|
||||
gFFI.userModel.getLoginResponseFromAuthBody(authBody);
|
||||
resp =
|
||||
gFFI.userModel.getLoginResponseFromAuthBody(authBody);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Failed to parse oidc login body: "$authBody"');
|
||||
}
|
||||
close(true);
|
||||
|
||||
if (resp != null) {
|
||||
handleLoginResponse(resp, false, null);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -572,6 +599,7 @@ Future<bool?> loginDialog() async {
|
||||
],
|
||||
),
|
||||
onCancel: onDialogCancel,
|
||||
onSubmit: onLogin,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -582,37 +610,23 @@ Future<bool?> loginDialog() async {
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<bool?> verificationCodeDialog(UserPayload? user) async {
|
||||
Future<bool?> verificationCodeDialog(
|
||||
UserPayload? user, String? secret, bool isEmailVerification) async {
|
||||
var autoLogin = true;
|
||||
var isInProgress = false;
|
||||
String? errorText;
|
||||
|
||||
final code = TextEditingController();
|
||||
final focusNode = FocusNode()..requestFocus();
|
||||
Timer(Duration(milliseconds: 100), () => focusNode..requestFocus());
|
||||
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||
bool validate() {
|
||||
return code.text.length >= 6;
|
||||
}
|
||||
|
||||
code.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() => errorText = null);
|
||||
}
|
||||
});
|
||||
|
||||
void onVerify() async {
|
||||
if (!validate()) {
|
||||
setState(
|
||||
() => errorText = translate('Too short, at least 6 characters.'));
|
||||
return;
|
||||
}
|
||||
setState(() => isInProgress = true);
|
||||
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(LoginRequest(
|
||||
verificationCode: code.text,
|
||||
tfaCode: isEmailVerification ? null : code.text,
|
||||
secret: secret,
|
||||
username: user?.name,
|
||||
id: await bind.mainGetMyId(),
|
||||
uuid: await bind.mainGetUuid(),
|
||||
@@ -641,27 +655,37 @@ Future<bool?> verificationCodeDialog(UserPayload? user) async {
|
||||
setState(() => isInProgress = false);
|
||||
}
|
||||
|
||||
final codeField = isEmailVerification
|
||||
? DialogEmailCodeField(
|
||||
controller: code,
|
||||
errorText: errorText,
|
||||
readyCallback: onVerify,
|
||||
onChanged: () => errorText = null,
|
||||
)
|
||||
: Dialog2FaField(
|
||||
controller: code,
|
||||
errorText: errorText,
|
||||
readyCallback: onVerify,
|
||||
onChanged: () => errorText = null,
|
||||
);
|
||||
|
||||
getOnSubmit() => codeField.isReady ? onVerify : null;
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Verification code")),
|
||||
contentBoxConstraints: BoxConstraints(maxWidth: 300),
|
||||
content: Column(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: user?.email == null,
|
||||
offstage: !isEmailVerification || user?.email == null,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Email", prefixIcon: Icon(Icons.email)),
|
||||
readOnly: true,
|
||||
controller: TextEditingController(text: user?.email),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
DialogTextField(
|
||||
title: '${translate("Verification code")}:',
|
||||
controller: code,
|
||||
errorText: errorText,
|
||||
focusNode: focusNode,
|
||||
helperText: translate('verification_tip'),
|
||||
),
|
||||
isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
|
||||
codeField,
|
||||
/*
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
@@ -682,10 +706,10 @@ Future<bool?> verificationCodeDialog(UserPayload? user) async {
|
||||
],
|
||||
),
|
||||
onCancel: close,
|
||||
onSubmit: onVerify,
|
||||
onSubmit: getOnSubmit(),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("Verify", onPressed: onVerify),
|
||||
dialogButton("Verify", onPressed: getOnSubmit()),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -27,45 +27,44 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return isIOS
|
||||
? IOSDraggable (
|
||||
position: position,
|
||||
chatModel: chatModel,
|
||||
width: width,
|
||||
height: height,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildMobileAppBar(context),
|
||||
Expanded(
|
||||
child: ChatPage(chatModel: chatModel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
chatModel: chatModel,
|
||||
builder: (context, onPanUpdate) {
|
||||
final child =
|
||||
Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: CustomAppBar(
|
||||
onPanUpdate: onPanUpdate,
|
||||
appBar: isDesktop
|
||||
? _buildDesktopAppBar(context)
|
||||
: _buildMobileAppBar(context),
|
||||
? IOSDraggable(
|
||||
position: position,
|
||||
chatModel: chatModel,
|
||||
width: width,
|
||||
height: height,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildMobileAppBar(context),
|
||||
Expanded(
|
||||
child: ChatPage(chatModel: chatModel),
|
||||
),
|
||||
body: ChatPage(chatModel: chatModel),
|
||||
);
|
||||
return Container(
|
||||
decoration:
|
||||
BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
child: child);
|
||||
});
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
chatModel: chatModel,
|
||||
builder: (context, onPanUpdate) {
|
||||
final child = Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: CustomAppBar(
|
||||
onPanUpdate: onPanUpdate,
|
||||
appBar: isDesktop
|
||||
? _buildDesktopAppBar(context)
|
||||
: _buildMobileAppBar(context),
|
||||
),
|
||||
body: ChatPage(chatModel: chatModel),
|
||||
);
|
||||
return Container(
|
||||
decoration:
|
||||
BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
child: child);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildMobileAppBar(BuildContext context) {
|
||||
@@ -354,14 +353,14 @@ class _DraggableState extends State<Draggable> {
|
||||
}
|
||||
|
||||
class IOSDraggable extends StatefulWidget {
|
||||
const IOSDraggable({
|
||||
Key? key,
|
||||
this.position = Offset.zero,
|
||||
this.chatModel,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.builder})
|
||||
: super(key: key);
|
||||
const IOSDraggable(
|
||||
{Key? key,
|
||||
this.position = Offset.zero,
|
||||
this.chatModel,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.builder})
|
||||
: super(key: key);
|
||||
|
||||
final Offset position;
|
||||
final ChatModel? chatModel;
|
||||
@@ -370,10 +369,10 @@ class IOSDraggable extends StatefulWidget {
|
||||
final Widget Function(BuildContext) builder;
|
||||
|
||||
@override
|
||||
_IOSDraggableState createState() => _IOSDraggableState();
|
||||
IOSDraggableState createState() => IOSDraggableState();
|
||||
}
|
||||
|
||||
class _IOSDraggableState extends State<IOSDraggable> {
|
||||
class IOSDraggableState extends State<IOSDraggable> {
|
||||
late Offset _position;
|
||||
late ChatModel? _chatModel;
|
||||
late double _width;
|
||||
@@ -423,7 +422,7 @@ class _IOSDraggableState extends State<IOSDraggable> {
|
||||
_lastBottomHeight = bottomHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
checkKeyboard();
|
||||
return Stack(
|
||||
@@ -439,12 +438,12 @@ class _IOSDraggableState extends State<IOSDraggable> {
|
||||
_chatModel?.setChatWindowPosition(_position);
|
||||
},
|
||||
child: Material(
|
||||
child:
|
||||
Container(
|
||||
width: _width,
|
||||
height: _height,
|
||||
decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
child: widget.builder(context),
|
||||
child: Container(
|
||||
width: _width,
|
||||
height: _height,
|
||||
decoration:
|
||||
BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
child: widget.builder(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -499,6 +498,7 @@ class QualityMonitor extends StatelessWidget {
|
||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||
_row(
|
||||
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ import 'dart:math' as math;
|
||||
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
|
||||
Function(BuildContext);
|
||||
|
||||
enum PeerUiType { grid, list }
|
||||
enum PeerUiType { grid, tile, list }
|
||||
|
||||
final peerCardUiType = PeerUiType.grid.obs;
|
||||
|
||||
@@ -495,7 +495,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
id,
|
||||
translate('Transfer File'),
|
||||
translate('Transfer file'),
|
||||
isFileTransfer: true,
|
||||
);
|
||||
}
|
||||
@@ -505,7 +505,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
id,
|
||||
translate('TCP Tunneling'),
|
||||
translate('TCP tunneling'),
|
||||
isTcpTunneling: true,
|
||||
);
|
||||
}
|
||||
@@ -568,7 +568,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
MenuEntryBase<String> _createShortCutAction(String id) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Create Desktop Shortcut'),
|
||||
translate('Create desktop shortcut'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
@@ -600,8 +600,9 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs);
|
||||
|
||||
_openInWindowsAction(String id) async => await _openNewConnInAction(
|
||||
id, 'Open in New Window', kOptionOpenInWindows);
|
||||
id, 'Open in new window', kOptionOpenInWindows);
|
||||
|
||||
// ignore: unused_element
|
||||
_openNewConnInOptAction(String id) async =>
|
||||
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
||||
? await _openInWindowsAction(id)
|
||||
@@ -818,7 +819,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
MenuEntryBase<String> _addToAb(Peer peer) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Add to Address Book'),
|
||||
translate('Add to address book'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
|
||||
@@ -75,9 +75,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
void initState() {
|
||||
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
|
||||
if (uiType != '') {
|
||||
peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index
|
||||
? PeerUiType.list
|
||||
: PeerUiType.grid;
|
||||
peerCardUiType.value = int.parse(uiType) == 0
|
||||
? PeerUiType.grid
|
||||
: int.parse(uiType) == 1
|
||||
? PeerUiType.tile
|
||||
: PeerUiType.list;
|
||||
}
|
||||
hideAbTagsPanel.value =
|
||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
||||
@@ -86,6 +88,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
|
||||
Future<void> handleTabSelection(int tabIndex) async {
|
||||
if (tabIndex < entries.length) {
|
||||
if (tabIndex != gFFI.peerTabModel.currentTab) {
|
||||
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
||||
}
|
||||
gFFI.peerTabModel.setCurrentTab(tabIndex);
|
||||
entries[tabIndex].load(hint: false);
|
||||
}
|
||||
@@ -215,29 +220,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
}
|
||||
|
||||
Widget _createPeerViewTypeSwitch(BuildContext context) {
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
final types = [PeerUiType.grid, PeerUiType.list];
|
||||
|
||||
return Obx(() => _hoverAction(
|
||||
context: context,
|
||||
onTap: () async {
|
||||
final type = types
|
||||
.elementAt(peerCardUiType.value == types.elementAt(0) ? 1 : 0);
|
||||
await bind.setLocalFlutterOption(
|
||||
k: 'peer-card-ui-type', v: type.index.toString());
|
||||
peerCardUiType.value = type;
|
||||
},
|
||||
child: Tooltip(
|
||||
message: peerCardUiType.value == PeerUiType.grid
|
||||
? translate('List View')
|
||||
: translate('Grid View'),
|
||||
child: Icon(
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? Icons.view_list_rounded
|
||||
: Icons.grid_view_rounded,
|
||||
size: 18,
|
||||
color: textColor,
|
||||
))));
|
||||
return PeerViewDropdown();
|
||||
}
|
||||
|
||||
Widget _createMultiSelection() {
|
||||
@@ -257,7 +240,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
"assets/checkbox-outline.svg",
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: textColor,
|
||||
colorFilter: svgColor(textColor),
|
||||
)),
|
||||
);
|
||||
}
|
||||
@@ -353,15 +336,26 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Widget createMultiSelectionBar() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
deleteSelection(),
|
||||
addSelectionToFav(),
|
||||
addSelectionToAb(),
|
||||
editSelectionTags(),
|
||||
Expanded(child: Container()),
|
||||
selectionCount(model.selectedPeers.length),
|
||||
selectAll(),
|
||||
closeSelection(),
|
||||
Offstage(
|
||||
offstage: model.selectedPeers.isEmpty,
|
||||
child: Row(
|
||||
children: [
|
||||
deleteSelection(),
|
||||
addSelectionToFav(),
|
||||
addSelectionToAb(),
|
||||
editSelectionTags(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
selectionCount(model.selectedPeers.length),
|
||||
selectAll(),
|
||||
closeSelection(),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -476,7 +470,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
});
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Add to Address Book'),
|
||||
message: translate('Add to address book'),
|
||||
child: Icon(model.icons[PeerTabIndex.ab.index])),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
);
|
||||
@@ -605,7 +599,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
"assets/chevron_up_chevron_down.svg",
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: textColor,
|
||||
colorFilter: svgColor(textColor),
|
||||
)),
|
||||
onTap: showMenu,
|
||||
);
|
||||
@@ -644,8 +638,6 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
searchWidth -
|
||||
(actions.length == 2 ? otherActionWidth : 0);
|
||||
final availablePositions = rightWidth ~/ otherActionWidth;
|
||||
debugPrint(
|
||||
"dynamic action count:${dynamicActions.length}, available positions: $availablePositions");
|
||||
|
||||
if (availablePositions < dynamicActions.length &&
|
||||
dynamicActions.length > 1) {
|
||||
@@ -777,6 +769,87 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
}
|
||||
}
|
||||
|
||||
class PeerViewDropdown extends StatefulWidget {
|
||||
const PeerViewDropdown({super.key});
|
||||
|
||||
@override
|
||||
State<PeerViewDropdown> createState() => _PeerViewDropdownState();
|
||||
}
|
||||
|
||||
class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<PeerUiType> types = [
|
||||
PeerUiType.grid,
|
||||
PeerUiType.tile,
|
||||
PeerUiType.list
|
||||
];
|
||||
final style = TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
List<PopupMenuEntry> items = List.empty(growable: true);
|
||||
items.add(PopupMenuItem(
|
||||
height: 36,
|
||||
enabled: false,
|
||||
child: Text(translate("Change view"), style: style)));
|
||||
for (var e in PeerUiType.values) {
|
||||
items.add(PopupMenuItem(
|
||||
height: 36,
|
||||
child: Obx(() => Center(
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: getRadio<PeerUiType>(
|
||||
Text(
|
||||
translate(types.indexOf(e) == 0
|
||||
? 'Big tiles'
|
||||
: types.indexOf(e) == 1
|
||||
? 'Small tiles'
|
||||
: 'List'),
|
||||
style: style),
|
||||
e,
|
||||
peerCardUiType.value,
|
||||
dense: true, (PeerUiType? v) async {
|
||||
if (v != null) {
|
||||
peerCardUiType.value = v;
|
||||
setState(() {});
|
||||
await bind.setLocalFlutterOption(
|
||||
k: "peer-card-ui-type",
|
||||
v: peerCardUiType.value.index.toString(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
))));
|
||||
}
|
||||
|
||||
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
child: Tooltip(
|
||||
message: translate('Change view'),
|
||||
child: Icon(
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? Icons.grid_view_rounded
|
||||
: peerCardUiType.value == PeerUiType.tile
|
||||
? Icons.view_list_rounded
|
||||
: Icons.view_agenda_rounded,
|
||||
size: 18,
|
||||
)),
|
||||
onTapDown: (details) {
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
onTap: () => showMenu(
|
||||
context: context,
|
||||
position: menuPos,
|
||||
items: items,
|
||||
elevation: 8,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class PeerSortDropdown extends StatefulWidget {
|
||||
const PeerSortDropdown({super.key});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/peer_model.dart';
|
||||
@@ -81,7 +82,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
final _curPeers = <String>{};
|
||||
var _lastChangeTime = DateTime.now();
|
||||
var _lastQueryPeers = <String>{};
|
||||
var _lastQueryTime = DateTime.now().subtract(const Duration(hours: 1));
|
||||
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
|
||||
var _queryCount = 0;
|
||||
var _exit = false;
|
||||
|
||||
@@ -188,12 +189,25 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
onVisibilityChanged: onVisibilityChanged,
|
||||
child: widget.peerCardBuilder(peer),
|
||||
);
|
||||
final windowWidth = MediaQuery.of(context).size.width;
|
||||
// `Provider.of<PeerTabModel>(context)` will causes infinete loop.
|
||||
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
|
||||
//
|
||||
// No need to listen the currentTab change event.
|
||||
// Because the currentTab change event will trigger the peers change event,
|
||||
// and the peers change event will trigger _buildPeersView().
|
||||
final currentTab = Provider.of<PeerTabModel>(context, listen: false).currentTab;
|
||||
final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
||||
return isDesktop
|
||||
? Obx(
|
||||
() => SizedBox(
|
||||
width: 220,
|
||||
width: peerCardUiType.value != PeerUiType.list
|
||||
? 220
|
||||
: currentTab == PeerTabIndex.group.index || (currentTab == PeerTabIndex.ab.index && !hideAbTagsPanel)
|
||||
? windowWidth - 390 :
|
||||
windowWidth - 227,
|
||||
height:
|
||||
peerCardUiType.value == PeerUiType.grid ? 140 : 42,
|
||||
peerCardUiType.value == PeerUiType.grid ? 140 : peerCardUiType.value != PeerUiType.list ? 42 : 45,
|
||||
child: visibilityChild,
|
||||
),
|
||||
)
|
||||
@@ -258,8 +272,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
if (_queryCount < _maxQueryCount) {
|
||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
platformFFI.ffiBind
|
||||
.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
_lastQueryTime = DateTime.now();
|
||||
_queryCount += 1;
|
||||
}
|
||||
@@ -273,7 +286,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
|
||||
_queryOnlines(bool isLoadEvent) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
platformFFI.ffiBind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
_lastQueryPeers = {..._curPeers};
|
||||
if (isLoadEvent) {
|
||||
_lastChangeTime = DateTime.now();
|
||||
|
||||
@@ -34,7 +34,8 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
canRequestFocus: true,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
onKey: inputModel.handleRawKeyEvent,
|
||||
onKey: (FocusNode data, RawKeyEvent e) =>
|
||||
inputModel.handleRawKeyEvent(e),
|
||||
child: child));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -10,11 +12,19 @@ customImageQualityWidget(
|
||||
required double initFps,
|
||||
required Function(double) setQuality,
|
||||
required Function(double) setFps,
|
||||
required bool showFps}) {
|
||||
required bool showFps,
|
||||
required bool showMoreQuality}) {
|
||||
if (initQuality < kMinQuality ||
|
||||
initQuality > (showMoreQuality ? kMaxMoreQuality : kMaxQuality)) {
|
||||
initQuality = kDefaultQuality;
|
||||
}
|
||||
if (initFps < kMinFps || initFps > kMaxFps) {
|
||||
initFps = kDefaultFps;
|
||||
}
|
||||
final qualityValue = initQuality.obs;
|
||||
final fpsValue = initFps.obs;
|
||||
|
||||
final RxBool moreQualityChecked = RxBool(qualityValue.value > 100);
|
||||
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
||||
final debouncerQuality = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
@@ -47,9 +57,11 @@ customImageQualityWidget(
|
||||
flex: 3,
|
||||
child: Slider(
|
||||
value: qualityValue.value,
|
||||
min: 10.0,
|
||||
max: moreQualityChecked.value ? 2000 : 100,
|
||||
divisions: moreQualityChecked.value ? 199 : 18,
|
||||
min: kMinQuality,
|
||||
max: moreQualityChecked.value ? kMaxMoreQuality : kMaxQuality,
|
||||
divisions: moreQualityChecked.value
|
||||
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
||||
: ((kMaxQuality - kMinQuality) / 5).round(),
|
||||
onChanged: (double value) async {
|
||||
qualityValue.value = value;
|
||||
debouncerQuality.value = value;
|
||||
@@ -69,7 +81,7 @@ customImageQualityWidget(
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
// mobile doesn't have enough space
|
||||
if (!isMobile)
|
||||
if (showMoreQuality && !isMobile)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
@@ -85,7 +97,7 @@ customImageQualityWidget(
|
||||
))
|
||||
],
|
||||
)),
|
||||
if (isMobile)
|
||||
if (showMoreQuality && isMobile)
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -109,9 +121,9 @@ customImageQualityWidget(
|
||||
flex: 3,
|
||||
child: Slider(
|
||||
value: fpsValue.value,
|
||||
min: 5.0,
|
||||
max: 120.0,
|
||||
divisions: 23,
|
||||
min: kMinFps,
|
||||
max: kMaxFps,
|
||||
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
||||
onChanged: (double value) async {
|
||||
fpsValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
@@ -141,15 +153,10 @@ customImageQualitySetting() {
|
||||
final fpsKey = 'custom-fps';
|
||||
|
||||
var initQuality =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? 50.0);
|
||||
if (initQuality < 10 || initQuality > 2000) {
|
||||
initQuality = 50;
|
||||
}
|
||||
var initFps =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0);
|
||||
if (initFps < 5 || initFps > 120) {
|
||||
initFps = 30;
|
||||
}
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
||||
kDefaultQuality);
|
||||
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
||||
kDefaultFps);
|
||||
|
||||
return customImageQualityWidget(
|
||||
initQuality: initQuality,
|
||||
@@ -160,59 +167,8 @@ customImageQualitySetting() {
|
||||
setFps: (v) {
|
||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||
},
|
||||
showFps: true);
|
||||
}
|
||||
|
||||
Future<bool> setServerConfig(
|
||||
List<TextEditingController> controllers,
|
||||
List<RxString> errMsgs,
|
||||
ServerConfig config,
|
||||
) async {
|
||||
config.idServer = config.idServer.trim();
|
||||
config.relayServer = config.relayServer.trim();
|
||||
config.apiServer = config.apiServer.trim();
|
||||
config.key = config.key.trim();
|
||||
// id
|
||||
if (config.idServer.isNotEmpty) {
|
||||
errMsgs[0].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.idServer));
|
||||
if (errMsgs[0].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// relay
|
||||
if (config.relayServer.isNotEmpty) {
|
||||
errMsgs[1].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.relayServer));
|
||||
if (errMsgs[1].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// api
|
||||
if (config.apiServer.isNotEmpty) {
|
||||
if (!config.apiServer.startsWith('http://') &&
|
||||
!config.apiServer.startsWith('https://')) {
|
||||
errMsgs[2].value =
|
||||
'${translate("API Server")}: ${translate("invalid_http")}';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final oldApiServer = await bind.mainGetApiServer();
|
||||
|
||||
// should set one by one
|
||||
await bind.mainSetOption(
|
||||
key: 'custom-rendezvous-server', value: config.idServer);
|
||||
await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
|
||||
await bind.mainSetOption(key: 'api-server', value: config.apiServer);
|
||||
await bind.mainSetOption(key: 'key', value: config.key);
|
||||
|
||||
final newApiServer = await bind.mainGetApiServer();
|
||||
if (oldApiServer.isNotEmpty &&
|
||||
oldApiServer != newApiServer &&
|
||||
gFFI.userModel.isLogin) {
|
||||
gFFI.userModel.logOut(apiServer: oldApiServer);
|
||||
}
|
||||
return true;
|
||||
showFps: true,
|
||||
showMoreQuality: true);
|
||||
}
|
||||
|
||||
List<Widget> ServerConfigImportExportWidgets(
|
||||
@@ -221,33 +177,7 @@ List<Widget> ServerConfigImportExportWidgets(
|
||||
) {
|
||||
import() {
|
||||
Clipboard.getData(Clipboard.kTextPlain).then((value) {
|
||||
final text = value?.text;
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
final sc = ServerConfig.decode(text);
|
||||
if (sc.idServer.isNotEmpty) {
|
||||
controllers[0].text = sc.idServer;
|
||||
controllers[1].text = sc.relayServer;
|
||||
controllers[2].text = sc.apiServer;
|
||||
controllers[3].text = sc.key;
|
||||
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
||||
success.then((value) {
|
||||
if (value) {
|
||||
showToast(
|
||||
translate('Import server configuration successfully'));
|
||||
} else {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
} else {
|
||||
showToast(translate('Clipboard is empty'));
|
||||
}
|
||||
importConfig(controllers, errMsgs, value?.text);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,7 +195,7 @@ List<Widget> ServerConfigImportExportWidgets(
|
||||
|
||||
return [
|
||||
Tooltip(
|
||||
message: translate('Import Server Config'),
|
||||
message: translate('Import server config'),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.paste, color: Colors.grey), onPressed: import),
|
||||
),
|
||||
@@ -275,3 +205,35 @@ List<Widget> ServerConfigImportExportWidgets(
|
||||
icon: Icon(Icons.copy, color: Colors.grey), onPressed: export))
|
||||
];
|
||||
}
|
||||
|
||||
List<(String, String)> otherDefaultSettings() {
|
||||
List<(String, String)> v = [
|
||||
('View Mode', 'view_only'),
|
||||
if (isDesktop) ('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||
if (isDesktop) ('Collapse toolbar', 'collapse_toolbar'),
|
||||
('Show remote cursor', 'show_remote_cursor'),
|
||||
if (isDesktop) ('Zoom cursor', 'zoom-cursor'),
|
||||
('Show quality monitor', 'show_quality_monitor'),
|
||||
('Mute', 'disable_audio'),
|
||||
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),
|
||||
('Disable clipboard', 'disable_clipboard'),
|
||||
('Lock after session end', 'lock_after_session_end'),
|
||||
('Privacy mode', 'privacy_mode'),
|
||||
if (isMobile) ('Touch mode', 'touch-mode'),
|
||||
('True color (4:4:4)', 'i444'),
|
||||
('Reverse mouse wheel', 'reverse_mouse_wheel'),
|
||||
('swap-left-right-mouse', 'swap-left-right-mouse'),
|
||||
if (isDesktop && useTextureRender)
|
||||
(
|
||||
'Show displays as individual windows',
|
||||
kKeyShowDisplaysAsIndividualWindows
|
||||
),
|
||||
if (isDesktop && useTextureRender)
|
||||
(
|
||||
'Use all my displays for the remote session',
|
||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||
)
|
||||
];
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter_hbb/common/widgets/dialog.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/models/desktop_render_texture.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
bool isEditOsPassword = false;
|
||||
@@ -87,29 +88,34 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// osAccount / osPassword
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Row(children: [
|
||||
Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
|
||||
Offstage(
|
||||
offstage: isDesktop,
|
||||
child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12),
|
||||
)
|
||||
]),
|
||||
trailingIcon: Transform.scale(
|
||||
scale: 0.8,
|
||||
child: InkWell(
|
||||
onTap: () => pi.is_headless
|
||||
? showSetOSAccount(sessionId, ffi.dialogManager)
|
||||
: handleOsPasswordEditIcon(sessionId, ffi.dialogManager),
|
||||
child: Icon(Icons.edit),
|
||||
if (perms['keyboard'] != false) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Row(children: [
|
||||
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
|
||||
]),
|
||||
trailingIcon: Transform.scale(
|
||||
scale: isDesktop ? 0.8 : 1,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (isMobile && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
if (pi.isHeadless) {
|
||||
showSetOSAccount(sessionId, ffi.dialogManager);
|
||||
} else {
|
||||
handleOsPasswordEditIcon(sessionId, ffi.dialogManager);
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null),
|
||||
),
|
||||
),
|
||||
onPressed: () => pi.isHeadless
|
||||
? showSetOSAccount(sessionId, ffi.dialogManager)
|
||||
: handleOsPasswordAction(sessionId, ffi.dialogManager),
|
||||
),
|
||||
onPressed: () => pi.is_headless
|
||||
? showSetOSAccount(sessionId, ffi.dialogManager)
|
||||
: handleOsPasswordAction(sessionId, ffi.dialogManager),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
// paste
|
||||
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||
v.add(TTextMenu(
|
||||
@@ -132,7 +138,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer File')),
|
||||
child: Text(translate('Transfer file')),
|
||||
onPressed: () => connect(context, id, isFileTransfer: true)),
|
||||
);
|
||||
}
|
||||
@@ -140,7 +146,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP Tunneling')),
|
||||
child: Text(translate('TCP tunneling')),
|
||||
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
||||
);
|
||||
}
|
||||
@@ -175,7 +181,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Restart Remote Device')),
|
||||
child: Text(translate('Restart remote device')),
|
||||
onPressed: () =>
|
||||
showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)),
|
||||
);
|
||||
@@ -190,6 +196,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
}
|
||||
// blockUserInput
|
||||
if (ffi.ffiModel.keyboard &&
|
||||
ffi.ffiModel.permissions['block_input'] != false &&
|
||||
pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
|
||||
{
|
||||
v.add(TTextMenu(
|
||||
@@ -208,7 +215,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
ffiModel.keyboard &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
pi.platform != kPeerPlatformMacOS &&
|
||||
version_cmp(pi.version, '1.2.0') >= 0) {
|
||||
versionCmp(pi.version, '1.2.0') >= 0 &&
|
||||
bind.peerGetDefaultSessionsCount(id: id) == 1) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Switch Sides')),
|
||||
onPressed: () =>
|
||||
@@ -217,15 +225,13 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
// refresh
|
||||
if (pi.version.isNotEmpty) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Refresh')),
|
||||
onPressed: () => bind.sessionRefresh(sessionId: sessionId)));
|
||||
child: Text(translate('Refresh')),
|
||||
onPressed: () => sessionRefreshVideo(sessionId, pi),
|
||||
));
|
||||
}
|
||||
// record
|
||||
var codecFormat = ffi.qualityMonitorModel.data.codecFormat;
|
||||
if (!isDesktop &&
|
||||
(ffi.recordingModel.start ||
|
||||
(perms["recording"] != false &&
|
||||
(codecFormat == "VP8" || codecFormat == "VP9")))) {
|
||||
(ffi.recordingModel.start || (perms["recording"] != false))) {
|
||||
v.add(TTextMenu(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -377,7 +383,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
// show remote cursor
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.is_wayland) {
|
||||
!pi.isWayland) {
|
||||
final state = ShowRemoteCursorState.find(id);
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'show-remote-cursor';
|
||||
@@ -436,19 +442,23 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Mute'))));
|
||||
}
|
||||
// file copy and paste
|
||||
if (Platform.isWindows &&
|
||||
pi.platform == kPeerPlatformWindows &&
|
||||
perms['file'] != false) {
|
||||
if (ffiModel.keyboard &&
|
||||
perms['file'] != false &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'enable-file-transfer';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
},
|
||||
child: Text(translate('Allow file copy and paste'))));
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
}
|
||||
: null,
|
||||
child: Text(translate('Enable file copy and paste'))));
|
||||
}
|
||||
// disable clipboard
|
||||
if (ffiModel.keyboard && perms['clipboard'] != false) {
|
||||
@@ -469,34 +479,158 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
}
|
||||
// lock after session end
|
||||
if (ffiModel.keyboard) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'lock-after-session-end';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
},
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
}
|
||||
: null,
|
||||
child: Text(translate('Lock after session end'))));
|
||||
}
|
||||
// privacy mode
|
||||
if (ffiModel.keyboard && pi.features.privacyMode) {
|
||||
final option = 'privacy-mode';
|
||||
final rxValue = PrivacyModeState.find(id);
|
||||
|
||||
if (useTextureRender &&
|
||||
pi.isSupportMultiDisplay &&
|
||||
PrivacyModeState.find(id).isEmpty &&
|
||||
pi.displaysCount.value > 1 &&
|
||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||
final value =
|
||||
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
v.add(TToggleMenu(
|
||||
value: rxValue.value,
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (ffiModel.pi.currentDisplay != 0) {
|
||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info',
|
||||
'Please switch to Display 1 first', '', ffi.dialogManager);
|
||||
return;
|
||||
}
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
bind.sessionSetDisplaysAsIndividualWindows(
|
||||
sessionId: sessionId, value: value ? 'Y' : '');
|
||||
},
|
||||
child: Text(translate('Privacy mode'))));
|
||||
child: Text(translate('Show displays as individual windows'))));
|
||||
}
|
||||
|
||||
final screenList = await getScreenRectList();
|
||||
if (useTextureRender && pi.isSupportMultiDisplay && screenList.length > 1) {
|
||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||
sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
|
||||
sessionId: sessionId, value: value ? 'Y' : '');
|
||||
},
|
||||
child: Text(translate('Use all my displays for the remote session'))));
|
||||
}
|
||||
|
||||
// 444
|
||||
final codec_format = ffi.qualityMonitorModel.data.codecFormat;
|
||||
if (versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||
(codec_format == "AV1" || codec_format == "VP9")) {
|
||||
final option = 'i444';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
bind.sessionChangePreferCodec(sessionId: sessionId);
|
||||
},
|
||||
child: Text(translate('True color (4:4:4)'))));
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
v.addAll(toolbarKeyboardToggles(ffi));
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1));
|
||||
|
||||
List<TToggleMenu> toolbarPrivacyMode(
|
||||
RxString privacyModeState, BuildContext context, String id, FFI ffi) {
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final sessionId = ffi.sessionId;
|
||||
|
||||
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
||||
final enabled = !ffi.ffiModel.viewOnly;
|
||||
return TToggleMenu(
|
||||
value: privacyModeState.isNotEmpty,
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
if (ffiModel.pi.currentDisplay != 0 &&
|
||||
ffiModel.pi.currentDisplay != kAllDisplayValue) {
|
||||
msgBox(
|
||||
sessionId,
|
||||
'custom-nook-nocancel-hasclose',
|
||||
'info',
|
||||
'Please switch to Display 1 first',
|
||||
'',
|
||||
ffi.dialogManager);
|
||||
return;
|
||||
}
|
||||
final option = 'privacy-mode';
|
||||
toggleFunc(sessionId, option);
|
||||
}
|
||||
: null,
|
||||
child: Text(translate('Privacy mode')));
|
||||
}
|
||||
|
||||
final privacyModeImpls =
|
||||
pi.platformAdditions[kPlatformAdditionsSupportedPrivacyModeImpl]
|
||||
as List<dynamic>?;
|
||||
if (privacyModeImpls == null) {
|
||||
return [
|
||||
getDefaultMenu((sid, opt) async {
|
||||
bind.sessionToggleOption(sessionId: sid, value: opt);
|
||||
togglePrivacyModeTime = DateTime.now();
|
||||
})
|
||||
];
|
||||
}
|
||||
if (privacyModeImpls.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (privacyModeImpls.length == 1) {
|
||||
final implKey = (privacyModeImpls[0] as List<dynamic>)[0] as String;
|
||||
return [
|
||||
getDefaultMenu((sid, opt) async {
|
||||
bind.sessionTogglePrivacyMode(
|
||||
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
|
||||
togglePrivacyModeTime = DateTime.now();
|
||||
})
|
||||
];
|
||||
} else {
|
||||
return privacyModeImpls.map((e) {
|
||||
final implKey = (e as List<dynamic>)[0] as String;
|
||||
final implName = (e)[1] as String;
|
||||
return TToggleMenu(
|
||||
child: Text(translate(implName)),
|
||||
value: privacyModeState.value == implKey,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
togglePrivacyModeTime = DateTime.now();
|
||||
bind.sessionTogglePrivacyMode(
|
||||
sessionId: sessionId, implKey: implKey, on: value);
|
||||
});
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final sessionId = ffi.sessionId;
|
||||
List<TToggleMenu> v = [];
|
||||
|
||||
// swap key
|
||||
if (ffiModel.keyboard &&
|
||||
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
|
||||
@@ -504,13 +638,53 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
final option = 'allow_swap_key';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
onChanged(bool? value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
}
|
||||
|
||||
final enabled = !ffi.ffiModel.viewOnly;
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
},
|
||||
onChanged: enabled ? onChanged : null,
|
||||
child: Text(translate('Swap control-command key'))));
|
||||
}
|
||||
|
||||
// reverse mouse wheel
|
||||
if (ffiModel.keyboard) {
|
||||
var optionValue =
|
||||
bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
|
||||
if (optionValue == '') {
|
||||
optionValue = bind.mainGetUserDefaultOption(key: 'reverse_mouse_wheel');
|
||||
}
|
||||
onChanged(bool? value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionSetReverseMouseWheel(
|
||||
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||
}
|
||||
|
||||
final enabled = !ffi.ffiModel.viewOnly;
|
||||
v.add(TToggleMenu(
|
||||
value: optionValue == 'Y',
|
||||
onChanged: enabled ? onChanged : null,
|
||||
child: Text(translate('Reverse mouse wheel'))));
|
||||
}
|
||||
|
||||
// swap left right mouse
|
||||
if (ffiModel.keyboard) {
|
||||
final option = 'swap-left-right-mouse';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
onChanged(bool? value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
}
|
||||
|
||||
final enabled = !ffi.ffiModel.viewOnly;
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: enabled ? onChanged : null,
|
||||
child: Text(translate('swap-left-right-mouse'))));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,28 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
const int kMaxVirtualDisplayCount = 4;
|
||||
const int kAllVirtualDisplay = -1;
|
||||
|
||||
const double kDesktopRemoteTabBarHeight = 28.0;
|
||||
const int kInvalidWindowId = -1;
|
||||
const int kMainWindowId = 0;
|
||||
|
||||
const kAllDisplayValue = -1;
|
||||
|
||||
const kKeyLegacyMode = 'legacy';
|
||||
const kKeyMapMode = 'map';
|
||||
const kKeyTranslateMode = 'translate';
|
||||
|
||||
const String kPlatformAdditionsIsWayland = "is_wayland";
|
||||
const String kPlatformAdditionsHeadless = "headless";
|
||||
const String kPlatformAdditionsIsInstalled = "is_installed";
|
||||
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
|
||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||
const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl";
|
||||
|
||||
const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
const String kPeerPlatformMacOS = "Mac OS";
|
||||
@@ -27,6 +44,8 @@ const String kAppTypeDesktopPortForward = "port forward";
|
||||
|
||||
const String kWindowMainWindowOnTop = "main_window_on_top";
|
||||
const String kWindowGetWindowInfo = "get_window_info";
|
||||
const String kWindowGetScreenList = "get_screen_list";
|
||||
// This method is not used, maybe it can be removed.
|
||||
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
|
||||
const String kWindowActionRebuild = "rebuild";
|
||||
const String kWindowEventHide = "hide";
|
||||
@@ -37,11 +56,13 @@ const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
|
||||
const String kWindowEventNewFileTransfer = "new_file_transfer";
|
||||
const String kWindowEventNewPortForward = "new_port_forward";
|
||||
const String kWindowEventActiveSession = "active_session";
|
||||
const String kWindowEventActiveDisplaySession = "active_display_session";
|
||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
||||
|
||||
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
||||
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
||||
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||
|
||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||
@@ -60,6 +81,12 @@ const int kWindowMainId = 0;
|
||||
const String kPointerEventKindTouch = "touch";
|
||||
const String kPointerEventKindMouse = "mouse";
|
||||
|
||||
const String kKeyShowDisplaysAsIndividualWindows =
|
||||
'displays_as_individual_windows';
|
||||
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||
'use_all_my_displays_for_the_remote_session';
|
||||
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
||||
|
||||
// the executable name of the portable version
|
||||
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
||||
|
||||
@@ -77,9 +104,26 @@ const int kDesktopMaxDisplaySize = 3840;
|
||||
const double kDesktopFileTransferRowHeight = 30.0;
|
||||
const double kDesktopFileTransferHeaderHeight = 25.0;
|
||||
|
||||
const double kMinFps = 5;
|
||||
const double kDefaultFps = 30;
|
||||
const double kMaxFps = 120;
|
||||
|
||||
const double kMinQuality = 10;
|
||||
const double kDefaultQuality = 50;
|
||||
const double kMaxQuality = 100;
|
||||
const double kMaxMoreQuality = 2000;
|
||||
|
||||
double kNewWindowOffset = Platform.isWindows
|
||||
? 56.0
|
||||
: Platform.isLinux
|
||||
? 50.0
|
||||
: Platform.isMacOS
|
||||
? 30.0
|
||||
: 50.0;
|
||||
|
||||
EdgeInsets get kDragToResizeAreaPadding =>
|
||||
!kUseCompatibleUiMode && Platform.isLinux
|
||||
? stateGlobal.fullscreen || stateGlobal.isMaximized.value
|
||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(5.0)
|
||||
: EdgeInsets.zero;
|
||||
@@ -162,6 +206,12 @@ const kRemoteAudioDualWay = 'dual-way';
|
||||
|
||||
const kIgnoreDpi = true;
|
||||
|
||||
// ================================ mobile ================================
|
||||
|
||||
// Magic numbers, maybe need to avoid it or use a better way to get them.
|
||||
const kMobileDelaySoftKeyboard = Duration(milliseconds: 30);
|
||||
const kMobileDelaySoftKeyboardFocus = Duration(milliseconds: 30);
|
||||
|
||||
/// Android constants
|
||||
const kActionApplicationDetailsSettings =
|
||||
"android.settings.APPLICATION_DETAILS_SETTINGS";
|
||||
|
||||
@@ -11,10 +11,12 @@ import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/formatter/id_formatter.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/autocomplete.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/button.dart';
|
||||
|
||||
@@ -35,12 +37,15 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
Timer? _updateTimer;
|
||||
|
||||
final RxBool _idInputFocused = false.obs;
|
||||
final FocusNode _idFocusNode = FocusNode();
|
||||
|
||||
var svcStopped = Get.find<RxBool>(tag: 'stop-service');
|
||||
var svcIsUsingPublicServer = true.obs;
|
||||
|
||||
bool isWindowMinimized = false;
|
||||
List<Peer> peers = [];
|
||||
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -58,12 +63,6 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
_updateTimer = periodic_immediate(Duration(seconds: 1), () async {
|
||||
updateStatus();
|
||||
});
|
||||
_idFocusNode.addListener(() {
|
||||
_idInputFocused.value = _idFocusNode.hasFocus;
|
||||
// select all to faciliate removing text, just following the behavior of address input of chrome
|
||||
_idController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: _idController.value.text.length);
|
||||
});
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
@@ -76,6 +75,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
}
|
||||
if (Get.isRegistered<TextEditingController>()) {
|
||||
Get.delete<TextEditingController>();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -142,8 +144,20 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
connect(context, id, isFileTransfer: isFileTransfer);
|
||||
}
|
||||
|
||||
Future<void> _fetchPeers() async {
|
||||
setState(() {
|
||||
isPeersLoading = true;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
peers = await getAllPeers();
|
||||
setState(() {
|
||||
isPeersLoading = false;
|
||||
isPeersLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
/// Search for a peer and connect to it if the id exists.
|
||||
/// Search for a peer.
|
||||
Widget _buildRemoteIDTextField(BuildContext context) {
|
||||
var w = Container(
|
||||
width: 320 + 20 * 2,
|
||||
@@ -157,51 +171,192 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AutoSizeText(
|
||||
translate('Control Remote Desktop'),
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
AutoSizeText(
|
||||
translate('Control Remote Desktop'),
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.merge(TextStyle(height: 1)),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 0),
|
||||
message: translate("id_input_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
maxLength: 90,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: _idFocusNode,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1.4,
|
||||
),
|
||||
maxLines: 1,
|
||||
cursorColor:
|
||||
Theme.of(context).textTheme.titleLarge?.color,
|
||||
decoration: InputDecoration(
|
||||
filled: false,
|
||||
counterText: '',
|
||||
hintText: _idInputFocused.value
|
||||
? null
|
||||
: translate('Enter Remote ID'),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 13)),
|
||||
controller: _idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
onSubmitted: (s) {
|
||||
onConnect();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Autocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.empty();
|
||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
alias: '',
|
||||
platform: '',
|
||||
tags: [],
|
||||
hash: '',
|
||||
forceAlwaysRelay: false,
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
);
|
||||
return [emptyPeer];
|
||||
} else {
|
||||
String textWithoutSpaces =
|
||||
textEditingValue.text.replaceAll(" ", "");
|
||||
if (int.tryParse(textWithoutSpaces) != null) {
|
||||
textEditingValue = TextEditingValue(
|
||||
text: textWithoutSpaces,
|
||||
selection: textEditingValue.selection,
|
||||
);
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
|
||||
return peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
.toLowerCase()
|
||||
.contains(textToFind) ||
|
||||
peer.hostname
|
||||
.toLowerCase()
|
||||
.contains(textToFind) ||
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
}
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
BuildContext context,
|
||||
TextEditingController fieldTextEditingController,
|
||||
FocusNode fieldFocusNode,
|
||||
VoidCallback onFieldSubmitted,
|
||||
) {
|
||||
fieldTextEditingController.text = _idController.text;
|
||||
Get.put<TextEditingController>(fieldTextEditingController);
|
||||
fieldFocusNode.addListener(() async {
|
||||
_idInputFocused.value = fieldFocusNode.hasFocus;
|
||||
if (fieldFocusNode.hasFocus && !isPeersLoading) {
|
||||
_fetchPeers();
|
||||
}
|
||||
});
|
||||
final textLength =
|
||||
fieldTextEditingController.value.text.length;
|
||||
// select all to facilitate removing text, just following the behavior of address input of chrome
|
||||
fieldTextEditingController.selection =
|
||||
TextSelection(baseOffset: 0, extentOffset: textLength);
|
||||
return Obx(() => TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: fieldFocusNode,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1.4,
|
||||
),
|
||||
maxLines: 1,
|
||||
cursorColor:
|
||||
Theme.of(context).textTheme.titleLarge?.color,
|
||||
decoration: InputDecoration(
|
||||
filled: false,
|
||||
counterText: '',
|
||||
hintText: _idInputFocused.value
|
||||
? null
|
||||
: translate('Enter Remote ID'),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 13)),
|
||||
controller: fieldTextEditingController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
onChanged: (v) {
|
||||
_idController.id = v;
|
||||
},
|
||||
));
|
||||
},
|
||||
onSelected: (option) {
|
||||
setState(() {
|
||||
_idController.id = option.id;
|
||||
FocusScope.of(context).unfocus();
|
||||
});
|
||||
},
|
||||
optionsViewBuilder: (BuildContext context,
|
||||
AutocompleteOnSelected<Peer> onSelected,
|
||||
Iterable<Peer> options) {
|
||||
double maxHeight = options.length * 50;
|
||||
if (options.length == 1) {
|
||||
maxHeight = 52;
|
||||
} else if (options.length == 3) {
|
||||
maxHeight = 146;
|
||||
} else if (options.length == 4) {
|
||||
maxHeight = 193;
|
||||
}
|
||||
maxHeight = maxHeight.clamp(0, 200);
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 5,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: 319,
|
||||
),
|
||||
child: peers.isEmpty && isPeersLoading
|
||||
? Container(
|
||||
height: 80,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
))
|
||||
: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 5),
|
||||
child: ListView(
|
||||
children: options
|
||||
.map((peer) =>
|
||||
AutocompletePeerTile(
|
||||
onSelect: () =>
|
||||
onSelected(peer),
|
||||
peer: peer))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
))),
|
||||
);
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
@@ -212,7 +367,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
Button(
|
||||
isOutline: true,
|
||||
onTap: () => onConnect(isFileTransfer: true),
|
||||
text: "Transfer File",
|
||||
text: "Transfer file",
|
||||
),
|
||||
const SizedBox(
|
||||
width: 17,
|
||||
@@ -265,7 +420,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
onTap: () async {
|
||||
await start_service(true);
|
||||
},
|
||||
child: Text(translate("Start Service"),
|
||||
child: Text(translate("Start service"),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: em)))
|
||||
@@ -308,7 +463,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
}
|
||||
|
||||
void onUsePublicServerGuide() {
|
||||
const url = "https://rustdesk.com/blog/id-relay-set/";
|
||||
const url = "https://rustdesk.com/pricing.html";
|
||||
canLaunchUrlString(url).then((can) {
|
||||
if (can) {
|
||||
launchUrlString(url);
|
||||
|
||||
@@ -187,12 +187,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Tooltip(
|
||||
message: translate('Settings'),
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
)),
|
||||
message: translate('Settings'),
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
)),
|
||||
),
|
||||
),
|
||||
onHover: (value) => hover.value = value,
|
||||
@@ -256,27 +256,27 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
))
|
||||
)),
|
||||
message: translate('Refresh Password'),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)))),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
InkWell(
|
||||
child: Obx(
|
||||
() => Tooltip(
|
||||
message: translate('Change Password'),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
color:
|
||||
editHover.value ? textColor : Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)).marginOnly(right: 8, top: 4),
|
||||
message: translate('Change Password'),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
color: editHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)).marginOnly(right: 8, top: 4),
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(1),
|
||||
onHover: (value) => editHover.value = value,
|
||||
@@ -329,19 +329,24 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
"Click to download", () async {
|
||||
final Uri url = Uri.parse('https://rustdesk.com/download');
|
||||
await launchUrl(url);
|
||||
},
|
||||
closeButton: true);
|
||||
}, closeButton: true);
|
||||
}
|
||||
if (systemError.isNotEmpty) {
|
||||
return buildInstallCard("", systemError, "", () {});
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (!bind.mainIsInstalled()) {
|
||||
return buildInstallCard(
|
||||
"", "install_tip", "Install", bind.mainGotoInstall);
|
||||
return buildInstallCard("", "install_tip", "Install", () async {
|
||||
await rustDeskWinManager.closeAllSubWindows();
|
||||
bind.mainGotoInstall();
|
||||
});
|
||||
} else if (bind.mainIsInstalledLowerVersion()) {
|
||||
return buildInstallCard("Status", "Your installation is lower version.",
|
||||
"Click to upgrade", bind.mainUpdateMe);
|
||||
return buildInstallCard(
|
||||
"Status", "Your installation is lower version.", "Click to upgrade",
|
||||
() async {
|
||||
await rustDeskWinManager.closeAllSubWindows();
|
||||
bind.mainUpdateMe();
|
||||
});
|
||||
}
|
||||
} else if (Platform.isMacOS) {
|
||||
if (!bind.mainIsCanScreenRecording(prompt: false)) {
|
||||
@@ -379,16 +384,42 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
// });
|
||||
// }
|
||||
} else if (Platform.isLinux) {
|
||||
final LinuxCards = <Widget>[];
|
||||
if (bind.isSelinuxEnforcing()) {
|
||||
// Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
|
||||
final keyShowSelinuxHelpTip = "show-selinux-help-tip";
|
||||
if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') {
|
||||
LinuxCards.add(buildInstallCard(
|
||||
"Warning",
|
||||
"selinux_tip",
|
||||
"",
|
||||
() async {},
|
||||
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
|
||||
help: 'Help',
|
||||
link:
|
||||
'https://rustdesk.com/docs/en/client/linux/#permissions-issue',
|
||||
closeButton: true,
|
||||
closeOption: keyShowSelinuxHelpTip,
|
||||
));
|
||||
}
|
||||
}
|
||||
if (bind.mainCurrentIsWayland()) {
|
||||
return buildInstallCard(
|
||||
LinuxCards.add(buildInstallCard(
|
||||
"Warning", "wayland_experiment_tip", "", () async {},
|
||||
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
|
||||
help: 'Help',
|
||||
link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required');
|
||||
link: 'https://rustdesk.com/docs/en/client/linux/#x11-required'));
|
||||
} else if (bind.mainIsLoginWayland()) {
|
||||
return buildInstallCard("Warning",
|
||||
LinuxCards.add(buildInstallCard("Warning",
|
||||
"Login screen using Wayland is not supported", "", () async {},
|
||||
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
|
||||
help: 'Help',
|
||||
link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen');
|
||||
link: 'https://rustdesk.com/docs/en/client/linux/#login-screen'));
|
||||
}
|
||||
if (LinuxCards.isNotEmpty) {
|
||||
return Column(
|
||||
children: LinuxCards,
|
||||
);
|
||||
}
|
||||
}
|
||||
return Container();
|
||||
@@ -396,102 +427,115 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
|
||||
Widget buildInstallCard(String title, String content, String btnText,
|
||||
GestureTapCallback onPressed,
|
||||
{String? help, String? link, bool? closeButton}) {
|
||||
|
||||
void closeCard() {
|
||||
setState(() {
|
||||
isCardClosed = true;
|
||||
});
|
||||
{double marginTop = 20.0,
|
||||
String? help,
|
||||
String? link,
|
||||
bool? closeButton,
|
||||
String? closeOption}) {
|
||||
void closeCard() async {
|
||||
if (closeOption != null) {
|
||||
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
||||
if (bind.mainGetLocalOption(key: closeOption) == 'N') {
|
||||
setState(() {
|
||||
isCardClosed = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
isCardClosed = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 20),
|
||||
margin: EdgeInsets.only(top: marginTop),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Color.fromARGB(255, 226, 66, 188),
|
||||
Color.fromARGB(255, 244, 114, 124),
|
||||
],
|
||||
)),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: (title.isNotEmpty
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: Text(
|
||||
translate(title),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15),
|
||||
).marginOnly(bottom: 6)),
|
||||
]
|
||||
: <Widget>[]) +
|
||||
<Widget>[
|
||||
Text(
|
||||
translate(content),
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 13),
|
||||
).marginOnly(bottom: 20)
|
||||
] +
|
||||
(btnText.isNotEmpty
|
||||
? <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FixedWidthButton(
|
||||
width: 150,
|
||||
padding: 8,
|
||||
isOutline: true,
|
||||
text: translate(btnText),
|
||||
textColor: Colors.white,
|
||||
borderColor: Colors.white,
|
||||
textSize: 20,
|
||||
radius: 10,
|
||||
onTap: onPressed,
|
||||
)
|
||||
])
|
||||
]
|
||||
: <Widget>[]) +
|
||||
(help != null
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: InkWell(
|
||||
onTap: () async =>
|
||||
await launchUrl(Uri.parse(link!)),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
Color.fromARGB(255, 226, 66, 188),
|
||||
Color.fromARGB(255, 244, 114, 124),
|
||||
],
|
||||
)),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: (title.isNotEmpty
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: Text(
|
||||
translate(help),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.white,
|
||||
fontSize: 12),
|
||||
)).marginOnly(top: 6)),
|
||||
]
|
||||
: <Widget>[]))),
|
||||
translate(title),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15),
|
||||
).marginOnly(bottom: 6)),
|
||||
]
|
||||
: <Widget>[]) +
|
||||
<Widget>[
|
||||
Text(
|
||||
translate(content),
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 13),
|
||||
).marginOnly(bottom: 20)
|
||||
] +
|
||||
(btnText.isNotEmpty
|
||||
? <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FixedWidthButton(
|
||||
width: 150,
|
||||
padding: 8,
|
||||
isOutline: true,
|
||||
text: translate(btnText),
|
||||
textColor: Colors.white,
|
||||
borderColor: Colors.white,
|
||||
textSize: 20,
|
||||
radius: 10,
|
||||
onTap: onPressed,
|
||||
)
|
||||
])
|
||||
]
|
||||
: <Widget>[]) +
|
||||
(help != null
|
||||
? <Widget>[
|
||||
Center(
|
||||
child: InkWell(
|
||||
onTap: () async =>
|
||||
await launchUrl(Uri.parse(link!)),
|
||||
child: Text(
|
||||
translate(help),
|
||||
style: TextStyle(
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
color: Colors.white,
|
||||
fontSize: 12),
|
||||
)).marginOnly(top: 6)),
|
||||
]
|
||||
: <Widget>[]))),
|
||||
),
|
||||
if (closeButton != null && closeButton == true)
|
||||
Positioned(
|
||||
top: 18,
|
||||
right: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
Positioned(
|
||||
top: 18,
|
||||
right: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: closeCard,
|
||||
),
|
||||
onPressed: closeCard,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -555,6 +599,22 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Get.put<RxBool>(svcStopped, tag: 'stop-service');
|
||||
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
|
||||
|
||||
screenToMap(window_size.Screen screen) => {
|
||||
'frame': {
|
||||
'l': screen.frame.left,
|
||||
't': screen.frame.top,
|
||||
'r': screen.frame.right,
|
||||
'b': screen.frame.bottom,
|
||||
},
|
||||
'visibleFrame': {
|
||||
'l': screen.visibleFrame.left,
|
||||
't': screen.visibleFrame.top,
|
||||
'r': screen.visibleFrame.right,
|
||||
'b': screen.visibleFrame.bottom,
|
||||
},
|
||||
'scaleFactor': screen.scaleFactor,
|
||||
};
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
debugPrint(
|
||||
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
@@ -563,24 +623,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
} else if (call.method == kWindowGetWindowInfo) {
|
||||
final screen = (await window_size.getWindowInfo()).screen;
|
||||
if (screen == null) {
|
||||
return "";
|
||||
return '';
|
||||
} else {
|
||||
return jsonEncode({
|
||||
'frame': {
|
||||
'l': screen.frame.left,
|
||||
't': screen.frame.top,
|
||||
'r': screen.frame.right,
|
||||
'b': screen.frame.bottom,
|
||||
},
|
||||
'visibleFrame': {
|
||||
'l': screen.visibleFrame.left,
|
||||
't': screen.visibleFrame.top,
|
||||
'r': screen.visibleFrame.right,
|
||||
'b': screen.visibleFrame.bottom,
|
||||
},
|
||||
'scaleFactor': screen.scaleFactor,
|
||||
});
|
||||
return jsonEncode(screenToMap(screen));
|
||||
}
|
||||
} else if (call.method == kWindowGetScreenList) {
|
||||
return jsonEncode(
|
||||
(await window_size.getScreenList()).map(screenToMap).toList());
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventShow) {
|
||||
@@ -604,8 +653,18 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
debugPrint("Failed to parse window id '${call.arguments}': $e");
|
||||
}
|
||||
if (windowId != null) {
|
||||
await rustDeskWinManager.moveTabToNewWindow(windowId, args[1], args[2]);
|
||||
await rustDeskWinManager.moveTabToNewWindow(
|
||||
windowId, args[1], args[2]);
|
||||
}
|
||||
} else if (call.method == kWindowEventOpenMonitorSession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final windowId = args['window_id'] as int;
|
||||
final peerId = args['peer_id'] as String;
|
||||
final display = args['display'] as int;
|
||||
final displayCount = args['display_count'] as int;
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
await rustDeskWinManager.openMonitorSession(
|
||||
windowId, peerId, display, displayCount, screenRect);
|
||||
}
|
||||
});
|
||||
_uniLinksSubscription = listenUniLinks();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -322,6 +323,7 @@ class _GeneralState extends State<_General> {
|
||||
'enable-confirm-closing-tabs',
|
||||
isServer: false),
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
|
||||
wallpaper(),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Open connection in new tab',
|
||||
@@ -348,11 +350,49 @@ class _GeneralState extends State<_General> {
|
||||
return _Card(title: 'Other', children: children);
|
||||
}
|
||||
|
||||
Widget wallpaper() {
|
||||
return futureBuilder(future: () async {
|
||||
final support = await bind.mainSupportRemoveWallpaper();
|
||||
return support;
|
||||
}(), hasData: (data) {
|
||||
if (data is bool && data == true) {
|
||||
final option = 'allow-remove-wallpaper';
|
||||
bool value = mainGetBoolOptionSync(option);
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: _OptionCheckBox(
|
||||
context,
|
||||
'Remove wallpaper during incoming sessions',
|
||||
option,
|
||||
update: () {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (value)
|
||||
_CountDownButton(
|
||||
text: 'Test',
|
||||
second: 5,
|
||||
onPressed: () {
|
||||
bind.mainTestWallpaper(second: 5);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Offstage();
|
||||
});
|
||||
}
|
||||
|
||||
Widget hwcodec() {
|
||||
final hwcodec = bind.mainHasHwcodec();
|
||||
final gpucodec = bind.mainHasGpucodec();
|
||||
return Offstage(
|
||||
offstage: !bind.mainHasHwcodec(),
|
||||
offstage: !(hwcodec || gpucodec),
|
||||
child: _Card(title: 'Hardware Codec', children: [
|
||||
_OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec'),
|
||||
_OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec')
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -474,7 +514,7 @@ class _GeneralState extends State<_General> {
|
||||
if (!keys.contains(currentKey)) {
|
||||
currentKey = '';
|
||||
}
|
||||
return _ComboBox(
|
||||
return ComboBox(
|
||||
keys: keys,
|
||||
values: values,
|
||||
initialKey: currentKey,
|
||||
@@ -526,6 +566,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
child: Column(children: [
|
||||
permissions(context),
|
||||
password(context),
|
||||
_Card(title: '2FA', children: [tfa()]),
|
||||
_Card(title: 'ID', children: [changeId()]),
|
||||
more(context),
|
||||
]),
|
||||
@@ -534,6 +575,45 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
)).marginOnly(bottom: _kListViewBottomMargin));
|
||||
}
|
||||
|
||||
Widget tfa() {
|
||||
bool enabled = !locked;
|
||||
// Simple temp wrapper for PR check
|
||||
tmpWrapper() {
|
||||
RxBool has2fa = bind.mainHasValid2FaSync().obs;
|
||||
update() async {
|
||||
has2fa.value = bind.mainHasValid2FaSync();
|
||||
}
|
||||
|
||||
onChanged(bool? checked) async {
|
||||
change2fa(callback: update);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
child: InkWell(
|
||||
child: Obx(() => Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: has2fa.value,
|
||||
onChanged: enabled ? onChanged : null)
|
||||
.marginOnly(right: 5),
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate('enable-2fa-title'),
|
||||
style:
|
||||
TextStyle(color: disabledTextColor(context, enabled)),
|
||||
))
|
||||
],
|
||||
)),
|
||||
),
|
||||
onTap: () {
|
||||
onChanged(!has2fa.value);
|
||||
},
|
||||
).marginOnly(left: _kCheckBoxLeftMargin);
|
||||
}
|
||||
|
||||
return tmpWrapper();
|
||||
}
|
||||
|
||||
Widget changeId() {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: gFFI.serverModel,
|
||||
@@ -574,7 +654,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
|
||||
return _Card(title: 'Permissions', children: [
|
||||
_ComboBox(
|
||||
ComboBox(
|
||||
keys: [
|
||||
'',
|
||||
'full',
|
||||
@@ -593,23 +673,27 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
}).marginOnly(left: _kContentHMargin),
|
||||
Column(
|
||||
children: [
|
||||
_OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard',
|
||||
_OptionCheckBox(context, 'Enable keyboard/mouse', 'enable-keyboard',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard',
|
||||
_OptionCheckBox(context, 'Enable clipboard', 'enable-clipboard',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable File Transfer', 'enable-file-transfer',
|
||||
context, 'Enable file transfer', 'enable-file-transfer',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable Audio', 'enable-audio',
|
||||
_OptionCheckBox(context, 'Enable audio', 'enable-audio',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel',
|
||||
_OptionCheckBox(context, 'Enable TCP tunneling', 'enable-tunnel',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable Remote Restart', 'enable-remote-restart',
|
||||
context, 'Enable remote restart', 'enable-remote-restart',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable Recording Session', 'enable-record-session',
|
||||
context, 'Enable recording session', 'enable-record-session',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
if (Platform.isWindows)
|
||||
_OptionCheckBox(
|
||||
context, 'Enable blocking user input', 'enable-block-input',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable remote configuration modification',
|
||||
'allow-remote-config-modification',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
@@ -677,7 +761,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: _disabledTextColor(
|
||||
color: disabledTextColor(
|
||||
context, onChanged != null)),
|
||||
),
|
||||
],
|
||||
@@ -697,7 +781,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
final usePassword = model.approveMode != 'click';
|
||||
|
||||
return _Card(title: 'Password', children: [
|
||||
_ComboBox(
|
||||
ComboBox(
|
||||
enabled: !locked,
|
||||
keys: modeKeys,
|
||||
values: modeValues,
|
||||
@@ -730,7 +814,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
bool enabled = !locked;
|
||||
return _Card(title: 'Security', children: [
|
||||
shareRdp(context, enabled),
|
||||
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
|
||||
_OptionCheckBox(context, 'Deny LAN discovery', 'enable-lan-discovery',
|
||||
reverse: true, enabled: enabled),
|
||||
...directIp(context),
|
||||
whitelist(),
|
||||
@@ -757,7 +841,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
Expanded(
|
||||
child: Text(translate('Enable RDP session sharing'),
|
||||
style:
|
||||
TextStyle(color: _disabledTextColor(context, enabled))),
|
||||
TextStyle(color: disabledTextColor(context, enabled))),
|
||||
)
|
||||
],
|
||||
).marginOnly(left: _kCheckBoxLeftMargin),
|
||||
@@ -770,7 +854,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
update() => setState(() {});
|
||||
RxBool applyEnabled = false.obs;
|
||||
return [
|
||||
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
|
||||
_OptionCheckBox(context, 'Enable direct IP access', 'direct-server',
|
||||
update: update, enabled: !locked),
|
||||
() {
|
||||
// Simple temp wrapper for PR check
|
||||
@@ -860,7 +944,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
child: Text(
|
||||
translate('Use IP Whitelisting'),
|
||||
style:
|
||||
TextStyle(color: _disabledTextColor(context, enabled)),
|
||||
TextStyle(color: disabledTextColor(context, enabled)),
|
||||
))
|
||||
],
|
||||
)),
|
||||
@@ -904,7 +988,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
child: Text(
|
||||
translate('Hide connection management window'),
|
||||
style: TextStyle(
|
||||
color: _disabledTextColor(
|
||||
color: disabledTextColor(
|
||||
context, enabled && enableHideCm)),
|
||||
),
|
||||
),
|
||||
@@ -1016,8 +1100,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
// Simple temp wrapper for PR check
|
||||
tmpWrapper() {
|
||||
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
|
||||
Map<String, dynamic> oldOptions =
|
||||
jsonDecode(bind.mainGetOptionsSync() as String);
|
||||
Map<String, dynamic> oldOptions = jsonDecode(bind.mainGetOptionsSync());
|
||||
old(String key) {
|
||||
return (oldOptions[key] ?? '').trim();
|
||||
}
|
||||
@@ -1044,7 +1127,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
submit() async {
|
||||
bool result = await setServerConfig(
|
||||
controllers,
|
||||
null,
|
||||
errMsgs,
|
||||
ServerConfig(
|
||||
idServer: idController.text,
|
||||
@@ -1108,6 +1191,7 @@ class _DisplayState extends State<_Display> {
|
||||
scrollStyle(context),
|
||||
imageQuality(context),
|
||||
codec(context),
|
||||
privacyModeImpl(context),
|
||||
other(context),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin));
|
||||
}
|
||||
@@ -1247,6 +1331,42 @@ class _DisplayState extends State<_Display> {
|
||||
]);
|
||||
}
|
||||
|
||||
Widget privacyModeImpl(BuildContext context) {
|
||||
final supportedPrivacyModeImpls = bind.mainSupportedPrivacyModeImpls();
|
||||
late final List<dynamic> privacyModeImpls;
|
||||
try {
|
||||
privacyModeImpls = jsonDecode(supportedPrivacyModeImpls);
|
||||
} catch (e) {
|
||||
debugPrint('failed to parse supported privacy mode impls, err=$e');
|
||||
return Offstage();
|
||||
}
|
||||
if (privacyModeImpls.length < 2) {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
final key = 'privacy-mode-impl-key';
|
||||
onChanged(String value) async {
|
||||
await bind.mainSetOption(key: key, value: value);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String groupValue = bind.mainGetOptionSync(key: key);
|
||||
if (groupValue.isEmpty) {
|
||||
groupValue = bind.mainDefaultPrivacyModeImpl();
|
||||
}
|
||||
return _Card(
|
||||
title: 'Privacy mode',
|
||||
children: privacyModeImpls.map((impl) {
|
||||
final d = impl as List<dynamic>;
|
||||
return _Radio(context,
|
||||
value: d[0] as String,
|
||||
groupValue: groupValue,
|
||||
label: d[1] as String,
|
||||
onChanged: onChanged);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget otherRow(String label, String key) {
|
||||
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
||||
onChanged(bool b) async {
|
||||
@@ -1268,20 +1388,9 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
Widget other(BuildContext context) {
|
||||
return _Card(title: 'Other Default Options', children: [
|
||||
otherRow('View Mode', 'view_only'),
|
||||
otherRow('show_monitors_tip', 'show_monitors_toolbar'),
|
||||
otherRow('Collapse toolbar', 'collapse_toolbar'),
|
||||
otherRow('Show remote cursor', 'show_remote_cursor'),
|
||||
otherRow('Zoom cursor', 'zoom-cursor'),
|
||||
otherRow('Show quality monitor', 'show_quality_monitor'),
|
||||
otherRow('Mute', 'disable_audio'),
|
||||
otherRow('Allow file copy and paste', 'enable_file_transfer'),
|
||||
otherRow('Disable clipboard', 'disable_clipboard'),
|
||||
otherRow('Lock after session end', 'lock_after_session_end'),
|
||||
otherRow('Privacy mode', 'privacy_mode'),
|
||||
otherRow('Reverse mouse wheel', 'reverse_mouse_wheel'),
|
||||
]);
|
||||
final children =
|
||||
otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList();
|
||||
return _Card(title: 'Other Default Options', children: children);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1577,12 +1686,6 @@ Widget _Card(
|
||||
);
|
||||
}
|
||||
|
||||
Color? _disabledTextColor(BuildContext context, bool enabled) {
|
||||
return enabled
|
||||
? null
|
||||
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
Widget _OptionCheckBox(BuildContext context, String label, String key,
|
||||
{Function()? update,
|
||||
@@ -1631,7 +1734,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate(label),
|
||||
style: TextStyle(color: _disabledTextColor(context, enabled)),
|
||||
style: TextStyle(color: disabledTextColor(context, enabled)),
|
||||
))
|
||||
],
|
||||
),
|
||||
@@ -1668,7 +1771,7 @@ Widget _Radio<T>(BuildContext context,
|
||||
overflow: autoNewLine ? null : TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: _kContentFontSize,
|
||||
color: _disabledTextColor(context, enabled)))
|
||||
color: disabledTextColor(context, enabled)))
|
||||
.marginOnly(left: 5),
|
||||
),
|
||||
],
|
||||
@@ -1718,7 +1821,7 @@ Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
|
||||
children: [
|
||||
Text(
|
||||
'${translate(label)}: ',
|
||||
style: TextStyle(color: _disabledTextColor(context, enabled)),
|
||||
style: TextStyle(color: disabledTextColor(context, enabled)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 10,
|
||||
@@ -1782,7 +1885,7 @@ _LabeledTextField(
|
||||
'${translate(label)}:',
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: _disabledTextColor(context, enabled)),
|
||||
fontSize: 16, color: disabledTextColor(context, enabled)),
|
||||
).marginOnly(right: 10)),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
@@ -1792,84 +1895,73 @@ _LabeledTextField(
|
||||
decoration: InputDecoration(
|
||||
errorText: errorText.isNotEmpty ? errorText : null),
|
||||
style: TextStyle(
|
||||
color: _disabledTextColor(context, enabled),
|
||||
color: disabledTextColor(context, enabled),
|
||||
)),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 8);
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class _ComboBox extends StatelessWidget {
|
||||
late final List<String> keys;
|
||||
late final List<String> values;
|
||||
late final String initialKey;
|
||||
late final Function(String key) onChanged;
|
||||
late final bool enabled;
|
||||
late String current;
|
||||
|
||||
_ComboBox({
|
||||
class _CountDownButton extends StatefulWidget {
|
||||
_CountDownButton({
|
||||
Key? key,
|
||||
required this.keys,
|
||||
required this.values,
|
||||
required this.initialKey,
|
||||
required this.onChanged,
|
||||
this.enabled = true,
|
||||
required this.text,
|
||||
required this.second,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
final String text;
|
||||
final VoidCallback? onPressed;
|
||||
final int second;
|
||||
|
||||
@override
|
||||
State<_CountDownButton> createState() => _CountDownButtonState();
|
||||
}
|
||||
|
||||
class _CountDownButtonState extends State<_CountDownButton> {
|
||||
bool _isButtonDisabled = false;
|
||||
|
||||
late int _countdownSeconds = widget.second;
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startCountdownTimer() {
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (_countdownSeconds <= 0) {
|
||||
setState(() {
|
||||
_isButtonDisabled = false;
|
||||
});
|
||||
timer.cancel();
|
||||
} else {
|
||||
setState(() {
|
||||
_countdownSeconds--;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var index = keys.indexOf(initialKey);
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
var ref = values[index].obs;
|
||||
current = keys[index];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: enabled
|
||||
? MyTheme.color(context).border2 ?? MyTheme.border
|
||||
: MyTheme.border,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8), //border raiuds of dropdown button
|
||||
return ElevatedButton(
|
||||
onPressed: _isButtonDisabled
|
||||
? null
|
||||
: () {
|
||||
widget.onPressed?.call();
|
||||
setState(() {
|
||||
_isButtonDisabled = true;
|
||||
_countdownSeconds = widget.second;
|
||||
});
|
||||
_startCountdownTimer();
|
||||
},
|
||||
child: Text(
|
||||
_isButtonDisabled ? '$_countdownSeconds s' : translate(widget.text),
|
||||
),
|
||||
height: 42, // should be the height of a TextField
|
||||
child: Obx(() => DropdownButton<String>(
|
||||
isExpanded: true,
|
||||
value: ref.value,
|
||||
elevation: 16,
|
||||
underline: Container(),
|
||||
style: TextStyle(
|
||||
color: enabled
|
||||
? Theme.of(context).textTheme.titleMedium?.color
|
||||
: _disabledTextColor(context, enabled)),
|
||||
icon: const Icon(
|
||||
Icons.expand_more_sharp,
|
||||
size: 20,
|
||||
).marginOnly(right: 15),
|
||||
onChanged: enabled
|
||||
? (String? newValue) {
|
||||
if (newValue != null && newValue != ref.value) {
|
||||
ref.value = newValue;
|
||||
current = newValue;
|
||||
onChanged(keys[values.indexOf(newValue)]);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
items: values.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: _kContentFontSize),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).marginOnly(left: 15),
|
||||
);
|
||||
}).toList(),
|
||||
)),
|
||||
).marginOnly(bottom: 5);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
@@ -91,7 +91,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
});
|
||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
debugPrint("File manager page init success with id ${widget.id}");
|
||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||
@@ -104,7 +104,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
_ffi.close();
|
||||
_ffi.dialogManager.dismissAll();
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.disable();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||
});
|
||||
@@ -182,10 +182,9 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemoteToLocal ? pi : 0,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
child: SvgPicture.asset("assets/arrow.svg",
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context).tabBarTheme.labelColor)),
|
||||
).paddingOnly(left: 15),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
@@ -262,7 +261,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
"assets/refresh.svg",
|
||||
color: Colors.white,
|
||||
colorFilter: svgColor(Colors.white),
|
||||
),
|
||||
color: MyTheme.accent,
|
||||
hoverColor: MyTheme.accent80,
|
||||
@@ -272,7 +271,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
child: SvgPicture.asset(
|
||||
"assets/close.svg",
|
||||
color: Colors.white,
|
||||
colorFilter: svgColor(Colors.white),
|
||||
),
|
||||
onPressed: () {
|
||||
jobController.jobTable.removeAt(index);
|
||||
@@ -307,13 +306,14 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/transfer.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context).tabBarTheme.labelColor),
|
||||
height: 40,
|
||||
).paddingOnly(bottom: 10),
|
||||
Text(
|
||||
translate("No transfers in progress"),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.20,
|
||||
textScaler: TextScaler.linear(1.20),
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).tabBarTheme.labelColor),
|
||||
@@ -522,7 +522,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
quarterTurns: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter:
|
||||
svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
@@ -537,7 +538,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
quarterTurns: 3,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter:
|
||||
svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
@@ -603,7 +605,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
"assets/search.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter:
|
||||
svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
@@ -613,7 +616,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
onPressed: null,
|
||||
child: SvgPicture.asset(
|
||||
"assets/close.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter:
|
||||
svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
color: Theme.of(context).disabledColor,
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
@@ -626,7 +630,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
"assets/close.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter:
|
||||
svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
@@ -642,7 +647,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
"assets/refresh.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter:
|
||||
svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
@@ -666,7 +672,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
"assets/home.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter:
|
||||
svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
@@ -692,7 +699,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset("assets/folder_new.svg",
|
||||
color: MyTheme.accent),
|
||||
colorFilter: svgColor(MyTheme.accent)),
|
||||
Text(
|
||||
translate("Create Folder"),
|
||||
).paddingOnly(
|
||||
@@ -734,7 +741,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
"assets/folder_new.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter:
|
||||
svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
@@ -749,7 +757,8 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
: null,
|
||||
child: SvgPicture.asset(
|
||||
"assets/trash.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
@@ -795,24 +804,24 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
quarterTurns: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
color: selectedItems.items.isEmpty
|
||||
colorFilter: svgColor(selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? MyTheme.grayBg
|
||||
: MyTheme.darkGray
|
||||
: Colors.white,
|
||||
: Colors.white),
|
||||
alignment: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
label: isLocal
|
||||
? SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
color: selectedItems.items.isEmpty
|
||||
colorFilter: svgColor(selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? MyTheme.grayBg
|
||||
: MyTheme.darkGray
|
||||
: Colors.white,
|
||||
: Colors.white),
|
||||
)
|
||||
: Text(
|
||||
translate('Receive'),
|
||||
@@ -889,7 +898,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
"assets/dots.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
color: Theme.of(context).cardColor,
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
@@ -1000,9 +1009,10 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
entry.isFile
|
||||
? "assets/file.svg"
|
||||
: "assets/folder.svg",
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor,
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(entry.name.nonBreaking,
|
||||
@@ -1127,9 +1137,13 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
|
||||
Entry entry, bool isLocal) {
|
||||
final isCtrlDown = RawKeyboard.instance.keysPressed
|
||||
.contains(LogicalKeyboardKey.controlLeft);
|
||||
final isShiftDown =
|
||||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft);
|
||||
.contains(LogicalKeyboardKey.controlLeft) ||
|
||||
RawKeyboard.instance.keysPressed
|
||||
.contains(LogicalKeyboardKey.controlRight);
|
||||
final isShiftDown = RawKeyboard.instance.keysPressed
|
||||
.contains(LogicalKeyboardKey.shiftLeft) ||
|
||||
RawKeyboard.instance.keysPressed
|
||||
.contains(LogicalKeyboardKey.shiftRight);
|
||||
if (isCtrlDown) {
|
||||
if (selectedItems.items.contains(entry)) {
|
||||
selectedItems.remove(entry);
|
||||
@@ -1444,7 +1458,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
_locationStatus.value == LocationStatus.pathLocation
|
||||
? "assets/folder.svg"
|
||||
: "assets/search.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -66,7 +64,6 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
late final TextEditingController controller;
|
||||
final RxBool startmenu = true.obs;
|
||||
final RxBool desktopicon = true.obs;
|
||||
final RxBool driverCert = true.obs;
|
||||
final RxBool showProgress = false.obs;
|
||||
final RxBool btnEnabled = true.obs;
|
||||
|
||||
@@ -158,10 +155,6 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
Option(startmenu, label: 'Create start menu shortcuts')
|
||||
.marginOnly(bottom: 7),
|
||||
Option(desktopicon, label: 'Create desktop icon'),
|
||||
Offstage(
|
||||
offstage: !Platform.isWindows,
|
||||
child: Option(driverCert, label: 'install_cert_tip'),
|
||||
).marginOnly(top: 7),
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -253,43 +246,9 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
String args = '';
|
||||
if (startmenu.value) args += ' startmenu';
|
||||
if (desktopicon.value) args += ' desktopicon';
|
||||
if (driverCert.value) args += ' driverCert';
|
||||
bind.installInstallMe(options: args, path: controller.text);
|
||||
}
|
||||
|
||||
if (driverCert.isTrue) {
|
||||
final tag = 'install-info-install-cert-confirm';
|
||||
final btns = [
|
||||
OutlinedButton.icon(
|
||||
icon: Icon(Icons.close_rounded, size: 16),
|
||||
label: Text(translate('Cancel')),
|
||||
onPressed: () => gFFI.dialogManager.dismissByTag(tag),
|
||||
style: buttonStyle,
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(Icons.done_rounded, size: 16),
|
||||
label: Text(translate('OK')),
|
||||
onPressed: () {
|
||||
gFFI.dialogManager.dismissByTag(tag);
|
||||
do_install();
|
||||
},
|
||||
style: buttonStyle,
|
||||
)
|
||||
];
|
||||
gFFI.dialogManager.show(
|
||||
(setState, close, context) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: SelectionArea(
|
||||
child:
|
||||
msgboxContent('info', 'Warning', 'confirm_install_cert_tip')),
|
||||
actions: btns,
|
||||
onCancel: close,
|
||||
),
|
||||
tag: tag,
|
||||
);
|
||||
} else {
|
||||
do_install();
|
||||
}
|
||||
do_install();
|
||||
}
|
||||
|
||||
void selectInstallPath() async {
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:flutter_custom_cursor/cursor_manager.dart'
|
||||
as custom_cursor_manager;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
|
||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../../common/widgets/overlay.dart';
|
||||
import '../../common/widgets/remote_input.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/toolbar.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/desktop_render_texture.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
@@ -28,6 +29,7 @@ import '../widgets/tabbar_widget.dart';
|
||||
|
||||
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
||||
|
||||
// Used to skip session close if "move to new window" is clicked.
|
||||
final Map<String, bool> closeSessionOnDispose = {};
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
@@ -36,6 +38,8 @@ class RemotePage extends StatefulWidget {
|
||||
required this.id,
|
||||
required this.sessionId,
|
||||
required this.tabWindowId,
|
||||
required this.display,
|
||||
required this.displays,
|
||||
required this.password,
|
||||
required this.toolbarState,
|
||||
required this.tabController,
|
||||
@@ -46,6 +50,8 @@ class RemotePage extends StatefulWidget {
|
||||
final String id;
|
||||
final SessionID? sessionId;
|
||||
final int? tabWindowId;
|
||||
final int? display;
|
||||
final List<int>? displays;
|
||||
final String? password;
|
||||
final ToolbarState toolbarState;
|
||||
final String? switchUuid;
|
||||
@@ -73,9 +79,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
late RxBool _zoomCursor;
|
||||
late RxBool _remoteCursorMoved;
|
||||
late RxBool _keyboardEnabled;
|
||||
late RenderTexture _renderTexture;
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
var _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||
|
||||
@@ -109,6 +114,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
switchUuid: widget.switchUuid,
|
||||
forceRelay: widget.forceRelay,
|
||||
tabWindowId: widget.tabWindowId,
|
||||
display: widget.display,
|
||||
displays: widget.displays,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
@@ -116,11 +123,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
// Register texture.
|
||||
_renderTexture = RenderTexture();
|
||||
_renderTexture.create(sessionId);
|
||||
|
||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
@@ -179,7 +183,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +192,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
void onWindowMaximize() {
|
||||
super.onWindowMaximize();
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +200,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
void onWindowMinimize() {
|
||||
super.onWindowMinimize();
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.disable();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +211,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
// https://github.com/flutter/flutter/issues/64935
|
||||
super.dispose();
|
||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||
await _renderTexture.destroy(closeSession);
|
||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||
// ensure we leave this session, this is a double check
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
DesktopMultiWindow.removeListener(this);
|
||||
@@ -222,7 +226,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
overlays: SystemUiOverlay.values);
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
await Wakelock.disable();
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
await Get.delete<FFI>(tag: widget.id);
|
||||
removeSharedStates(widget.id);
|
||||
@@ -245,10 +249,11 @@ class _RemotePageState extends State<RemotePage>
|
||||
onEnterOrLeaveImageSetter: (func) =>
|
||||
_onEnterOrLeaveImage4Toolbar = func,
|
||||
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
|
||||
setRemoteState: setState,
|
||||
);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: Stack(
|
||||
|
||||
bodyWidget() {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.black,
|
||||
@@ -274,26 +279,55 @@ class _RemotePageState extends State<RemotePage>
|
||||
},
|
||||
inputModel: _ffi.inputModel,
|
||||
child: getBodyForDesktop(context))),
|
||||
Obx(() => Stack(
|
||||
children: [
|
||||
_ffi.ffiModel.pi.isSet.isTrue &&
|
||||
_ffi.ffiModel.waitForFirstImage.isTrue
|
||||
? emptyOverlay()
|
||||
: () {
|
||||
_ffi.ffiModel.tryShowAndroidActionsOverlay();
|
||||
return Offstage();
|
||||
}(),
|
||||
// Use Overlay to enable rebuild every time on menu button click.
|
||||
_ffi.ffiModel.pi.isSet.isTrue
|
||||
? Overlay(initialEntries: [
|
||||
OverlayEntry(builder: remoteToolbar)
|
||||
])
|
||||
: remoteToolbar(context),
|
||||
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
|
||||
],
|
||||
)),
|
||||
Stack(
|
||||
children: [
|
||||
_ffi.ffiModel.pi.isSet.isTrue &&
|
||||
_ffi.ffiModel.waitForFirstImage.isTrue
|
||||
? emptyOverlay()
|
||||
: () {
|
||||
_ffi.ffiModel.tryShowAndroidActionsOverlay();
|
||||
return Offstage();
|
||||
}(),
|
||||
// Use Overlay to enable rebuild every time on menu button click.
|
||||
_ffi.ffiModel.pi.isSet.isTrue
|
||||
? Overlay(
|
||||
initialEntries: [OverlayEntry(builder: remoteToolbar)])
|
||||
: remoteToolbar(context),
|
||||
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: Obx(() {
|
||||
final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
|
||||
_ffi.ffiModel.waitForFirstImage.isFalse;
|
||||
if (imageReady) {
|
||||
// If the privacy mode(disable physical displays) is switched,
|
||||
// we should not dismiss the dialog immediately.
|
||||
if (DateTime.now().difference(togglePrivacyModeTime) >
|
||||
const Duration(milliseconds: 3000)) {
|
||||
// `dismissAll()` is to ensure that the state is clean.
|
||||
// It's ok to call dismissAll() here.
|
||||
_ffi.dialogManager.dismissAll();
|
||||
// Recreate the block state to refresh the state.
|
||||
_blockableOverlayState = BlockableOverlayState();
|
||||
_blockableOverlayState.applyFfi(_ffi);
|
||||
}
|
||||
// Block the whole `bodyWidget()` when dialog shows.
|
||||
return BlockableOverlay(
|
||||
underlying: bodyWidget(),
|
||||
state: _blockableOverlayState,
|
||||
);
|
||||
} else {
|
||||
// `_blockableOverlayState` is not recreated here.
|
||||
// The toolbar's block state won't work properly when reconnecting, but that's okay.
|
||||
return bodyWidget();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -402,16 +436,23 @@ class _RemotePageState extends State<RemotePage>
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
});
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
remoteCursorMoved: _remoteCursorMoved,
|
||||
textureId: _renderTexture.textureId,
|
||||
useTextureRender: RenderTexture.useTextureRender,
|
||||
listenerBuilder: (child) =>
|
||||
_buildRawTouchAndPointerRegion(child, enterView, leaveView),
|
||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||
return Obx(
|
||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||
? Container(color: Colors.transparent)
|
||||
: Obx(() {
|
||||
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
remoteCursorMoved: _remoteCursorMoved,
|
||||
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
|
||||
child, enterView, leaveView),
|
||||
ffi: _ffi,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}))
|
||||
];
|
||||
@@ -442,24 +483,22 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
class ImagePaint extends StatefulWidget {
|
||||
final FFI ffi;
|
||||
final String id;
|
||||
final RxBool zoomCursor;
|
||||
final RxBool cursorOverImage;
|
||||
final RxBool keyboardEnabled;
|
||||
final RxBool remoteCursorMoved;
|
||||
final RxInt textureId;
|
||||
final bool useTextureRender;
|
||||
final Widget Function(Widget)? listenerBuilder;
|
||||
|
||||
ImagePaint(
|
||||
{Key? key,
|
||||
required this.ffi,
|
||||
required this.id,
|
||||
required this.zoomCursor,
|
||||
required this.cursorOverImage,
|
||||
required this.keyboardEnabled,
|
||||
required this.remoteCursorMoved,
|
||||
required this.textureId,
|
||||
required this.useTextureRender,
|
||||
this.listenerBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@@ -469,8 +508,6 @@ class ImagePaint extends StatefulWidget {
|
||||
|
||||
class _ImagePaintState extends State<ImagePaint> {
|
||||
bool _lastRemoteCursorMoved = false;
|
||||
final ScrollController _horizontal = ScrollController();
|
||||
final ScrollController _vertical = ScrollController();
|
||||
|
||||
String get id => widget.id;
|
||||
RxBool get zoomCursor => widget.zoomCursor;
|
||||
@@ -479,6 +516,11 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
|
||||
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
@@ -530,83 +572,99 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
});
|
||||
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final imageWidth = c.getDisplayWidth() * s;
|
||||
final imageHeight = c.getDisplayHeight() * s;
|
||||
final imageSize = Size(imageWidth, imageHeight);
|
||||
late final Widget imageWidget;
|
||||
if (widget.useTextureRender) {
|
||||
imageWidget = SizedBox(
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
child: Obx(() => Texture(
|
||||
textureId: widget.textureId.value,
|
||||
filterQuality:
|
||||
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
imageWidget = CustomPaint(
|
||||
size: imageSize,
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
);
|
||||
}
|
||||
|
||||
final paintWidth = c.getDisplayWidth() * s;
|
||||
final paintHeight = c.getDisplayHeight() * s;
|
||||
final paintSize = Size(paintWidth, paintHeight);
|
||||
final paintWidget = useTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.hasClients
|
||||
? _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
_horizontal.position.extentAfter)
|
||||
: 0.0;
|
||||
final percentY = _vertical.hasClients
|
||||
? _vertical.position.extentBefore /
|
||||
(_vertical.position.extentBefore +
|
||||
_vertical.position.extentInside +
|
||||
_vertical.position.extentAfter)
|
||||
: 0.0;
|
||||
c.setScrollPercent(percentX, percentY);
|
||||
c.updateScrollPercent();
|
||||
return false;
|
||||
},
|
||||
child: mouseRegion(
|
||||
child: Obx(() => _buildCrossScrollbarFromLayout(
|
||||
context, _buildListener(imageWidget), c.size, imageSize)),
|
||||
context,
|
||||
_buildListener(paintWidget),
|
||||
c.size,
|
||||
paintSize,
|
||||
c.scrollHorizontal,
|
||||
c.scrollVertical,
|
||||
)),
|
||||
));
|
||||
} else {
|
||||
late final Widget imageWidget;
|
||||
if (c.size.width > 0 && c.size.height > 0) {
|
||||
if (widget.useTextureRender) {
|
||||
final x = Platform.isLinux ? c.x.toInt().toDouble() : c.x;
|
||||
final y = Platform.isLinux ? c.y.toInt().toDouble() : c.y;
|
||||
imageWidget = Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: x,
|
||||
top: y,
|
||||
width: c.getDisplayWidth() * s,
|
||||
height: c.getDisplayHeight() * s,
|
||||
child: Texture(
|
||||
textureId: widget.textureId.value,
|
||||
filterQuality:
|
||||
isViewOriginal() ? FilterQuality.none : FilterQuality.low,
|
||||
final paintWidget = useTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c,
|
||||
s,
|
||||
Offset(
|
||||
Platform.isLinux ? c.x.toInt().toDouble() : c.x,
|
||||
Platform.isLinux ? c.y.toInt().toDouble() : c.y,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
imageWidget = CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
painter:
|
||||
ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||
);
|
||||
}
|
||||
return mouseRegion(child: _buildListener(imageWidget));
|
||||
c.size,
|
||||
isViewOriginal())
|
||||
: _buildScrollAuthNonTextureRender(m, c, s);
|
||||
return mouseRegion(child: _buildListener(paintWidget));
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildScrollbarNonTextureRender(
|
||||
ImageModel m, Size imageSize, double s) {
|
||||
return CustomPaint(
|
||||
size: imageSize,
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScrollAuthNonTextureRender(
|
||||
ImageModel m, CanvasModel c, double s) {
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _BuildPaintTextureRender(
|
||||
CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
|
||||
final ffiModel = c.parent.target!.ffiModel;
|
||||
final displays = ffiModel.pi.getCurDisplays();
|
||||
final children = <Widget>[];
|
||||
final rect = ffiModel.rect;
|
||||
if (rect == null) {
|
||||
return Container();
|
||||
}
|
||||
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
|
||||
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,
|
||||
child: Obx(() => Texture(
|
||||
textureId: textureId.value,
|
||||
filterQuality:
|
||||
isViewOriginal ? FilterQuality.none : FilterQuality.low,
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
return SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: Stack(children: children),
|
||||
);
|
||||
}
|
||||
|
||||
MouseCursor _buildCursorOfCache(
|
||||
CursorModel cursor, double scale, CursorData? cache) {
|
||||
if (cache == null) {
|
||||
@@ -614,7 +672,8 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
} else {
|
||||
final key = cache.updateGetKey(scale);
|
||||
if (!cursor.cachedKeys.contains(key)) {
|
||||
debugPrint("Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
|
||||
debugPrint(
|
||||
"Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
|
||||
// [Safety]
|
||||
// It's ok to call async registerCursor in current synchronous context,
|
||||
// because activating the cursor is also an async call and will always
|
||||
@@ -646,7 +705,13 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
}
|
||||
|
||||
Widget _buildCrossScrollbarFromLayout(
|
||||
BuildContext context, Widget child, Size layoutSize, Size size) {
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
Size layoutSize,
|
||||
Size size,
|
||||
ScrollController horizontal,
|
||||
ScrollController vertical,
|
||||
) {
|
||||
final scrollConfig = CustomMouseWheelScrollConfig(
|
||||
scrollDuration: kDefaultScrollDuration,
|
||||
scrollCurve: Curves.linearToEaseOut,
|
||||
@@ -658,7 +723,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
widget = ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: SingleChildScrollView(
|
||||
controller: _horizontal,
|
||||
controller: horizontal,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: cursorOverImage.isTrue
|
||||
? const NeverScrollableScrollPhysics()
|
||||
@@ -680,7 +745,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
widget = ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: SingleChildScrollView(
|
||||
controller: _vertical,
|
||||
controller: vertical,
|
||||
physics: cursorOverImage.isTrue
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
@@ -699,13 +764,13 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
}
|
||||
if (layoutSize.width < size.width) {
|
||||
widget = ImprovedScrolling(
|
||||
scrollController: _horizontal,
|
||||
scrollController: horizontal,
|
||||
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
|
||||
customMouseWheelScrollConfig: scrollConfig,
|
||||
child: RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: _horizontal,
|
||||
controller: horizontal,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
notificationPredicate: layoutSize.height < size.height
|
||||
@@ -717,13 +782,13 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
}
|
||||
if (layoutSize.height < size.height) {
|
||||
widget = ImprovedScrolling(
|
||||
scrollController: _vertical,
|
||||
scrollController: vertical,
|
||||
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
|
||||
customMouseWheelScrollConfig: scrollConfig,
|
||||
child: RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: _vertical,
|
||||
controller: vertical,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
child: widget,
|
||||
@@ -731,7 +796,11 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
);
|
||||
}
|
||||
|
||||
return widget;
|
||||
return Container(
|
||||
child: widget,
|
||||
width: layoutSize.width,
|
||||
height: layoutSize.height,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListener(Widget child) {
|
||||
@@ -770,11 +839,20 @@ class CursorPaint extends StatelessWidget {
|
||||
double cy = c.y;
|
||||
if (c.viewStyle.style == kRemoteViewStyleOriginal &&
|
||||
c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
final d = c.parent.target!.ffiModel.display;
|
||||
final imageWidth = d.width * c.scale;
|
||||
final imageHeight = d.height * c.scale;
|
||||
cx = -imageWidth * c.scrollX;
|
||||
cy = -imageHeight * c.scrollY;
|
||||
final rect = c.parent.target!.ffiModel.rect;
|
||||
if (rect == null) {
|
||||
// unreachable!
|
||||
debugPrint('unreachable! The displays rect is null.');
|
||||
return Container();
|
||||
}
|
||||
if (cx < 0) {
|
||||
final imageWidth = rect.width * c.scale;
|
||||
cx = -imageWidth * c.scrollX;
|
||||
}
|
||||
if (cy < 0) {
|
||||
final imageHeight = rect.height * c.scale;
|
||||
cy = -imageHeight * c.scrollY;
|
||||
}
|
||||
}
|
||||
|
||||
double x = (m.x - hotx) * c.scale + cx;
|
||||
|
||||
@@ -48,6 +48,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
late ToolbarState _toolbarState;
|
||||
String? peerId;
|
||||
bool _isScreenRectSet = false;
|
||||
int? _display;
|
||||
|
||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||
|
||||
@@ -57,6 +59,12 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
peerId = params['id'];
|
||||
final sessionId = params['session_id'];
|
||||
final tabWindowId = params['tab_window_id'];
|
||||
final display = params['display'];
|
||||
final displays = params['displays'];
|
||||
final screenRect = parseParamScreenRect(params);
|
||||
_isScreenRectSet = screenRect != null;
|
||||
_display = display as int?;
|
||||
tryMoveToScreenAndSetFullscreen(screenRect);
|
||||
if (peerId != null) {
|
||||
ConnectionTypeState.init(peerId!);
|
||||
tabController.onSelected = (id) {
|
||||
@@ -80,6 +88,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
id: peerId!,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: params['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
@@ -109,11 +119,18 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final switchUuid = args['switch_uuid'];
|
||||
final sessionId = args['session_id'];
|
||||
final tabWindowId = args['tab_window_id'];
|
||||
final display = args['display'];
|
||||
final displays = args['displays'];
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
windowOnTop(windowId());
|
||||
tryMoveToScreenAndSetFullscreen(screenRect);
|
||||
if (tabController.length == 0) {
|
||||
if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
|
||||
// Show the hidden window.
|
||||
if (Platform.isMacOS && stateGlobal.closeOnFullscreen == true) {
|
||||
stateGlobal.setFullscreen(true);
|
||||
}
|
||||
// Reset the state
|
||||
stateGlobal.closeOnFullscreen = null;
|
||||
}
|
||||
ConnectionTypeState.init(id);
|
||||
_toolbarState.setShow(
|
||||
@@ -129,6 +146,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
id: id,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
@@ -137,7 +156,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
),
|
||||
));
|
||||
} else if (call.method == kWindowDisableGrabKeyboard) {
|
||||
stateGlobal.grabKeyboard = false;
|
||||
// ???
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
@@ -148,6 +167,15 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventActiveDisplaySession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final display = args['display'];
|
||||
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventGetRemoteList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => e.key)
|
||||
@@ -160,32 +188,37 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
.join(';');
|
||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||
// Ready to show new window and close old tab.
|
||||
final peerId = call.arguments;
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final close = args['close'];
|
||||
try {
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == peerId)
|
||||
.firstWhere((tab) => tab.key == id)
|
||||
.page as RemotePage;
|
||||
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to get cached session data: $e');
|
||||
}
|
||||
if (returnValue != null) {
|
||||
closeSessionOnDispose[peerId] = false;
|
||||
tabController.closeBy(peerId);
|
||||
if (close && returnValue != null) {
|
||||
closeSessionOnDispose[id] = false;
|
||||
tabController.closeBy(id);
|
||||
}
|
||||
}
|
||||
_update_remote_count();
|
||||
return returnValue;
|
||||
});
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(
|
||||
WindowType.RemoteDesktop,
|
||||
windowId: windowId(),
|
||||
peerId: tabController.state.value.tabs.isEmpty
|
||||
? null
|
||||
: tabController.state.value.tabs[0].key,
|
||||
);
|
||||
});
|
||||
if (!_isScreenRectSet) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(
|
||||
WindowType.RemoteDesktop,
|
||||
windowId: windowId(),
|
||||
peerId: tabController.state.value.tabs.isEmpty
|
||||
? null
|
||||
: tabController.state.value.tabs[0].key,
|
||||
display: _display,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -353,7 +386,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
menu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Restart Remote Device'),
|
||||
translate('Restart remote device'),
|
||||
style: style,
|
||||
),
|
||||
proc: () => showRestartRemoteDevice(
|
||||
@@ -432,6 +465,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
|
||||
loopCloseWindow();
|
||||
}
|
||||
ConnectionTypeState.delete(id);
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/cm_file_model.dart';
|
||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||
@@ -482,8 +483,8 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
client.type_() != ClientType.file),
|
||||
child: IconButton(
|
||||
onPressed: () => checkClickTime(client.id, () {
|
||||
if (client.type_() != ClientType.file) {
|
||||
gFFI.chatModel.toggleCMSidePage();
|
||||
if (client.type_() == ClientType.file) {
|
||||
gFFI.chatModel.toggleCMFilePage();
|
||||
} else {
|
||||
gFFI.chatModel
|
||||
.toggleCMChatPage(MessageKey(client.peerId, client.id));
|
||||
@@ -519,6 +520,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
Function(bool)? onTap, String tooltipText) {
|
||||
return Tooltip(
|
||||
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
|
||||
waitDuration: Duration.zero,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: enabled ? MyTheme.accent : Colors.grey[700],
|
||||
@@ -535,7 +537,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
child: Icon(
|
||||
iconData,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -547,9 +548,11 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final crossAxisCount = 4;
|
||||
final spacing = 10.0;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 200.0,
|
||||
height: 160.0,
|
||||
margin: EdgeInsets.all(5.0),
|
||||
padding: EdgeInsets.all(5.0),
|
||||
decoration: BoxDecoration(
|
||||
@@ -574,10 +577,10 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
).marginOnly(left: 4.0, bottom: 8.0),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
crossAxisCount: 3,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
mainAxisSpacing: 20.0,
|
||||
crossAxisSpacing: 20.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
padding: EdgeInsets.symmetric(horizontal: spacing),
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
children: [
|
||||
buildPermissionIcon(
|
||||
client.keyboard,
|
||||
@@ -589,7 +592,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.keyboard = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow using keyboard and mouse'),
|
||||
translate('Enable keyboard/mouse'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.clipboard,
|
||||
@@ -601,7 +604,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.clipboard = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow using clipboard'),
|
||||
translate('Enable clipboard'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.audio,
|
||||
@@ -613,7 +616,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.audio = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow hearing sound'),
|
||||
translate('Enable audio'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.file,
|
||||
@@ -625,7 +628,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.file = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow file copy and paste'),
|
||||
translate('Enable file copy and paste'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.restart,
|
||||
@@ -637,7 +640,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.restart = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow remote restart'),
|
||||
translate('Enable remote restart'),
|
||||
),
|
||||
buildPermissionIcon(
|
||||
client.recording,
|
||||
@@ -649,8 +652,24 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
client.recording = enabled;
|
||||
});
|
||||
},
|
||||
translate('Allow recording session'),
|
||||
)
|
||||
translate('Enable recording session'),
|
||||
),
|
||||
// only windows support block input
|
||||
if (Platform.isWindows)
|
||||
buildPermissionIcon(
|
||||
client.blockInput,
|
||||
Icons.block,
|
||||
(enabled) {
|
||||
bind.cmSwitchPermission(
|
||||
connId: client.id,
|
||||
name: "block_input",
|
||||
enabled: enabled);
|
||||
setState(() {
|
||||
client.blockInput = enabled;
|
||||
});
|
||||
},
|
||||
translate('Enable blocking user input'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -975,6 +994,49 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
);
|
||||
}
|
||||
|
||||
iconLabel(CmFileLog item) {
|
||||
switch (item.action) {
|
||||
case CmFileAction.none:
|
||||
return Container();
|
||||
case CmFileAction.localToRemote:
|
||||
case CmFileAction.remoteToLocal:
|
||||
return Column(
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.action == CmFileAction.remoteToLocal ? 0 : pi,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor),
|
||||
),
|
||||
),
|
||||
Text(item.action == CmFileAction.remoteToLocal
|
||||
? translate('Send')
|
||||
: translate('Receive'))
|
||||
],
|
||||
);
|
||||
case CmFileAction.remove:
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
Text(translate('Delete'))
|
||||
],
|
||||
);
|
||||
case CmFileAction.createDir:
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.create_new_folder,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
Text(translate('Create Folder'))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget statusList() {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size(200, double.infinity),
|
||||
@@ -983,7 +1045,7 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
child: Obx(
|
||||
() {
|
||||
final jobTable = gFFI.cmFileModel.currentJobTable;
|
||||
statusListView(List<JobProgress> jobs) => ListView.builder(
|
||||
statusListView(List<CmFileLog> jobs) => ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = jobs[index];
|
||||
@@ -998,22 +1060,7 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Column(
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemoteToLocal ? 0 : pi,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor,
|
||||
),
|
||||
),
|
||||
Text(item.isRemoteToLocal
|
||||
? translate('Send')
|
||||
: translate('Receive'))
|
||||
],
|
||||
),
|
||||
child: iconLabel(item),
|
||||
).paddingOnly(left: 15),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
@@ -1048,8 +1095,9 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage:
|
||||
item.state == JobState.inProgress,
|
||||
offstage: !(item.isTransfer() &&
|
||||
item.state !=
|
||||
JobState.inProgress),
|
||||
child: Text(
|
||||
translate(
|
||||
item.display(),
|
||||
@@ -1106,13 +1154,14 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/transfer.svg",
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context).tabBarTheme.labelColor),
|
||||
height: 40,
|
||||
).paddingOnly(bottom: 10),
|
||||
Text(
|
||||
translate("No transfers in progress"),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: 1.20,
|
||||
textScaler: TextScaler.linear(1.20),
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).tabBarTheme.labelColor),
|
||||
|
||||
@@ -12,9 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget {
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) {
|
||||
if (!bind.mainStartGrabKeyboard()) {
|
||||
stateGlobal.grabKeyboard = true;
|
||||
}
|
||||
bind.mainInitInputSource();
|
||||
stateGlobal.getInputSource(force: true);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' hide TabBarTheme;
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -74,8 +75,8 @@ CancelFunc showRightMenu(ToastBuilder builder,
|
||||
return BotToast.showAttachedWidget(
|
||||
target: target,
|
||||
targetContext: context,
|
||||
verticalOffset: 0,
|
||||
horizontalOffset: 0,
|
||||
verticalOffset: 0.0,
|
||||
horizontalOffset: 0.0,
|
||||
duration: Duration(seconds: 300),
|
||||
animationDuration: Duration(milliseconds: 0),
|
||||
animationReverseDuration: Duration(milliseconds: 0),
|
||||
@@ -150,18 +151,20 @@ class DesktopTabController {
|
||||
return false;
|
||||
}
|
||||
state.update((val) {
|
||||
val!.selected = index;
|
||||
Future.delayed(Duration(milliseconds: 100), (() {
|
||||
if (val.pageController.hasClients) {
|
||||
val.pageController.jumpToPage(index);
|
||||
}
|
||||
val.scrollController.itemCount = val.tabs.length;
|
||||
if (val.scrollController.hasClients &&
|
||||
val.scrollController.itemCount > index) {
|
||||
val.scrollController
|
||||
.scrollToItem(index, center: false, animate: true);
|
||||
}
|
||||
}));
|
||||
if (val != null) {
|
||||
val.selected = index;
|
||||
Future.delayed(Duration(milliseconds: 100), (() {
|
||||
if (val.pageController.hasClients) {
|
||||
val.pageController.jumpToPage(index);
|
||||
}
|
||||
val.scrollController.itemCount = val.tabs.length;
|
||||
if (val.scrollController.hasClients &&
|
||||
val.scrollController.itemCount > index) {
|
||||
val.scrollController
|
||||
.scrollToItem(index, center: false, animate: true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
if (callOnSelected) {
|
||||
if (state.value.tabs.length > index) {
|
||||
@@ -176,6 +179,19 @@ class DesktopTabController {
|
||||
jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
|
||||
callOnSelected: callOnSelected);
|
||||
|
||||
bool jumpToByKeyAndDisplay(String key, int display) {
|
||||
for (int i = 0; i < state.value.tabs.length; i++) {
|
||||
final tab = state.value.tabs[i];
|
||||
if (tab.key == key) {
|
||||
final ffi = (tab.page as RemotePage).ffi;
|
||||
if (ffi.ffiModel.pi.currentDisplay == display) {
|
||||
return jumpTo(i, callOnSelected: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void closeBy(String? key) {
|
||||
if (!isDesktop) return;
|
||||
assert(onRemoved != null);
|
||||
@@ -434,6 +450,7 @@ class DesktopTab extends StatelessWidget {
|
||||
isMainWindow: isMainWindow,
|
||||
tabType: tabType,
|
||||
state: state,
|
||||
tabController: controller,
|
||||
tail: tail,
|
||||
showMinimize: showMinimize,
|
||||
showMaximize: showMaximize,
|
||||
@@ -449,6 +466,7 @@ class WindowActionPanel extends StatefulWidget {
|
||||
final bool isMainWindow;
|
||||
final DesktopTabType tabType;
|
||||
final Rx<DesktopTabState> state;
|
||||
final DesktopTabController tabController;
|
||||
|
||||
final bool showMinimize;
|
||||
final bool showMaximize;
|
||||
@@ -461,6 +479,7 @@ class WindowActionPanel extends StatefulWidget {
|
||||
required this.isMainWindow,
|
||||
required this.tabType,
|
||||
required this.state,
|
||||
required this.tabController,
|
||||
this.tail,
|
||||
this.showMinimize = true,
|
||||
this.showMaximize = true,
|
||||
@@ -566,19 +585,25 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
void onWindowClose() async {
|
||||
mainWindowClose() async => await windowManager.hide();
|
||||
notMainWindowClose(WindowController controller) async {
|
||||
if (widget.tabController.length != 0) {
|
||||
debugPrint("close not emtpy multiwindow from taskbar");
|
||||
if (Platform.isWindows) {
|
||||
await controller.show();
|
||||
await controller.focus();
|
||||
final res = await widget.onClose?.call() ?? true;
|
||||
if (!res) return;
|
||||
}
|
||||
widget.tabController.clear();
|
||||
}
|
||||
await controller.hide();
|
||||
await Future.wait([
|
||||
rustDeskWinManager
|
||||
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
|
||||
widget.onClose?.call() ?? Future.microtask(() => null)
|
||||
]);
|
||||
await rustDeskWinManager
|
||||
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
||||
}
|
||||
|
||||
macOSWindowClose(
|
||||
Future<void> Function() restoreFunc,
|
||||
Future<bool> Function() checkFullscreen,
|
||||
Future<void> Function() closeFunc) async {
|
||||
await restoreFunc();
|
||||
Future<bool> Function() checkFullscreen,
|
||||
Future<void> Function() closeFunc,
|
||||
) async {
|
||||
_macOSCheckRestoreCounter = 0;
|
||||
_macOSCheckRestoreTimer =
|
||||
Timer.periodic(Duration(milliseconds: 30), (timer) async {
|
||||
@@ -598,26 +623,38 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
}
|
||||
// macOS specific workaround, the window is not hiding when in fullscreen.
|
||||
if (Platform.isMacOS && await windowManager.isFullScreen()) {
|
||||
stateGlobal.closeOnFullscreen = true;
|
||||
stateGlobal.closeOnFullscreen ??= true;
|
||||
await windowManager.setFullScreen(false);
|
||||
await macOSWindowClose(
|
||||
() async => await windowManager.setFullScreen(false),
|
||||
() async => await windowManager.isFullScreen(),
|
||||
mainWindowClose);
|
||||
() async => await windowManager.isFullScreen(),
|
||||
mainWindowClose,
|
||||
);
|
||||
} else {
|
||||
stateGlobal.closeOnFullscreen = false;
|
||||
stateGlobal.closeOnFullscreen ??= false;
|
||||
await mainWindowClose();
|
||||
}
|
||||
} else {
|
||||
// it's safe to hide the subwindow
|
||||
final controller = WindowController.fromWindowId(kWindowId!);
|
||||
if (Platform.isMacOS && await controller.isFullScreen()) {
|
||||
stateGlobal.closeOnFullscreen = true;
|
||||
await macOSWindowClose(
|
||||
() async => await controller.setFullscreen(false),
|
||||
() async => await controller.isFullScreen(),
|
||||
() async => await notMainWindowClose(controller));
|
||||
if (Platform.isMacOS) {
|
||||
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
|
||||
// use ??= to make sure the value is set on first call.
|
||||
|
||||
if (await widget.onClose?.call() ?? true) {
|
||||
if (await controller.isFullScreen()) {
|
||||
stateGlobal.closeOnFullscreen ??= true;
|
||||
await controller.setFullscreen(false);
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
await macOSWindowClose(
|
||||
() async => await controller.isFullScreen(),
|
||||
() async => await notMainWindowClose(controller),
|
||||
);
|
||||
} else {
|
||||
stateGlobal.closeOnFullscreen ??= false;
|
||||
await notMainWindowClose(controller);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stateGlobal.closeOnFullscreen = false;
|
||||
await notMainWindowClose(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ Future<void> main(List<String> args) async {
|
||||
debugPrint("--cm started");
|
||||
desktopType = DesktopType.cm;
|
||||
await windowManager.ensureInitialized();
|
||||
runConnectionManagerScreen(args.contains('--hide'));
|
||||
runConnectionManagerScreen();
|
||||
} else if (args.contains('--install')) {
|
||||
runInstallPage();
|
||||
} else {
|
||||
@@ -156,6 +156,7 @@ void runMobileApp() async {
|
||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
runApp(App());
|
||||
await initUniLinks();
|
||||
}
|
||||
|
||||
void runMultiWindow(
|
||||
@@ -198,8 +199,16 @@ void runMultiWindow(
|
||||
}
|
||||
switch (appType) {
|
||||
case kAppTypeDesktopRemote:
|
||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||
windowId: kWindowId!, peerId: argument['id'] as String?);
|
||||
// If screen rect is set, the window will be moved to the target screen and then set fullscreen.
|
||||
if (argument['screen_rect'] == null) {
|
||||
// display can be used to control the offset of the window.
|
||||
await restoreWindowPosition(
|
||||
WindowType.RemoteDesktop,
|
||||
windowId: kWindowId!,
|
||||
peerId: argument['id'] as String?,
|
||||
display: argument['display'] as int?,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case kAppTypeDesktopFileTransfer:
|
||||
await restoreWindowPosition(WindowType.FileTransfer,
|
||||
@@ -216,13 +225,14 @@ void runMultiWindow(
|
||||
WindowController.fromWindowId(kWindowId!).show();
|
||||
}
|
||||
|
||||
void runConnectionManagerScreen(bool hide) async {
|
||||
void runConnectionManagerScreen() async {
|
||||
await initEnv(kAppTypeConnectionManager);
|
||||
_runApp(
|
||||
'',
|
||||
const DesktopServerPage(),
|
||||
MyTheme.currentThemeMode(),
|
||||
);
|
||||
final hide = await bind.cmGetConfig(name: "hide_cm") == 'true';
|
||||
gFFI.serverModel.hideCm = hide;
|
||||
if (hide) {
|
||||
await hideCmWindow(isStartup: true);
|
||||
@@ -420,7 +430,7 @@ class _AppState extends State<App> {
|
||||
? (context, child) => AccessibilityListener(
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaleFactor: 1.0,
|
||||
textScaler: TextScaler.linear(1.0),
|
||||
),
|
||||
child: child ?? Container(),
|
||||
),
|
||||
@@ -442,7 +452,7 @@ class _AppState extends State<App> {
|
||||
Widget _keepScaleBuilder(BuildContext context, Widget? child) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaleFactor: 1.0,
|
||||
textScaler: TextScaler.linear(1.0),
|
||||
),
|
||||
child: child ?? Container(),
|
||||
);
|
||||
|
||||
@@ -6,10 +6,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/autocomplete.dart';
|
||||
import '../../consts.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
@@ -42,10 +44,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
|
||||
/// Update url. If it's not null, means an update is available.
|
||||
var _updateUrl = '';
|
||||
List<Peer> peers = [];
|
||||
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_uniLinksSubscription = listenUniLinks();
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
@@ -116,6 +124,18 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
color: Colors.white, fontWeight: FontWeight.bold))));
|
||||
}
|
||||
|
||||
Future<void> _fetchPeers() async {
|
||||
setState(() {
|
||||
isPeersLoading = true;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
peers = await getAllPeers();
|
||||
setState(() {
|
||||
isPeersLoading = false;
|
||||
isPeersLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
/// Search for a peer and connect to it if the id exists.
|
||||
Widget _buildRemoteIDTextField() {
|
||||
@@ -133,36 +153,162 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: AutoSizeTextField(
|
||||
minFontSize: 18,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
// keyboardType: TextInputType.number,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 30,
|
||||
color: MyTheme.idColor,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Remote ID'),
|
||||
// hintText: 'Enter your remote ID',
|
||||
border: InputBorder.none,
|
||||
helperStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.2,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
controller: _idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
child: Autocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.empty();
|
||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
username: '',
|
||||
hostname: '',
|
||||
alias: '',
|
||||
platform: '',
|
||||
tags: [],
|
||||
hash: '',
|
||||
forceAlwaysRelay: false,
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
);
|
||||
return [emptyPeer];
|
||||
} else {
|
||||
String textWithoutSpaces =
|
||||
textEditingValue.text.replaceAll(" ", "");
|
||||
if (int.tryParse(textWithoutSpaces) != null) {
|
||||
textEditingValue = TextEditingValue(
|
||||
text: textWithoutSpaces,
|
||||
selection: textEditingValue.selection,
|
||||
);
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
|
||||
return peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
.toLowerCase()
|
||||
.contains(textToFind) ||
|
||||
peer.hostname
|
||||
.toLowerCase()
|
||||
.contains(textToFind) ||
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
}
|
||||
},
|
||||
fieldViewBuilder: (BuildContext context,
|
||||
TextEditingController fieldTextEditingController,
|
||||
FocusNode fieldFocusNode,
|
||||
VoidCallback onFieldSubmitted) {
|
||||
fieldTextEditingController.text = _idController.text;
|
||||
fieldFocusNode.addListener(() async {
|
||||
_idEmpty.value =
|
||||
fieldTextEditingController.text.isEmpty;
|
||||
if (fieldFocusNode.hasFocus && !isPeersLoading) {
|
||||
_fetchPeers();
|
||||
}
|
||||
});
|
||||
final textLength =
|
||||
fieldTextEditingController.value.text.length;
|
||||
// select all to facilitate removing text, just following the behavior of address input of chrome
|
||||
fieldTextEditingController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: textLength);
|
||||
return AutoSizeTextField(
|
||||
controller: fieldTextEditingController,
|
||||
focusNode: fieldFocusNode,
|
||||
minFontSize: 18,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
// keyboardType: TextInputType.number,
|
||||
onChanged: (String text) {
|
||||
_idController.id = text;
|
||||
},
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 30,
|
||||
color: MyTheme.idColor,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Remote ID'),
|
||||
// hintText: 'Enter your remote ID',
|
||||
border: InputBorder.none,
|
||||
helperStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.2,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
);
|
||||
},
|
||||
onSelected: (option) {
|
||||
setState(() {
|
||||
_idController.id = option.id;
|
||||
FocusScope.of(context).unfocus();
|
||||
});
|
||||
},
|
||||
optionsViewBuilder: (BuildContext context,
|
||||
AutocompleteOnSelected<Peer> onSelected,
|
||||
Iterable<Peer> options) {
|
||||
double maxHeight = options.length * 50;
|
||||
if (options.length == 1) {
|
||||
maxHeight = 52;
|
||||
} else if (options.length == 3) {
|
||||
maxHeight = 146;
|
||||
} else if (options.length == 4) {
|
||||
maxHeight = 193;
|
||||
}
|
||||
maxHeight = maxHeight.clamp(0, 200);
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 5,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: 320,
|
||||
),
|
||||
child: peers.isEmpty && isPeersLoading
|
||||
? Container(
|
||||
height: 80,
|
||||
child: Center(
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
)))
|
||||
: ListView(
|
||||
padding:
|
||||
EdgeInsets.only(top: 5),
|
||||
children: options
|
||||
.map((peer) =>
|
||||
AutocompletePeerTile(
|
||||
onSelect: () =>
|
||||
onSelected(
|
||||
peer),
|
||||
peer: peer))
|
||||
.toList(),
|
||||
))))));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -170,7 +316,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
offstage: _idEmpty.value,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
_idController.clear();
|
||||
setState(() {
|
||||
_idController.clear();
|
||||
});
|
||||
},
|
||||
icon: Icon(Icons.clear, color: MyTheme.darkGray)),
|
||||
)),
|
||||
@@ -195,6 +343,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_uniLinksSubscription?.cancel();
|
||||
_idController.dispose();
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
|
||||
@@ -3,9 +3,10 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
@@ -73,7 +74,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, widget.id);
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -81,7 +82,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
model.close().whenComplete(() {
|
||||
gFFI.close();
|
||||
gFFI.dialogManager.dismissAll();
|
||||
Wakelock.disable();
|
||||
WakelockPlus.disable();
|
||||
});
|
||||
super.dispose();
|
||||
}
|
||||
@@ -661,6 +662,7 @@ class BottomSheetBody extends StatelessWidget {
|
||||
|
||||
@override
|
||||
BottomSheet build(BuildContext context) {
|
||||
// ignore: no_leading_underscores_for_local_identifiers
|
||||
final _actions = actions ?? [];
|
||||
return BottomSheet(
|
||||
builder: (BuildContext context) {
|
||||
|
||||
@@ -13,15 +13,15 @@ abstract class PageShape extends Widget {
|
||||
}
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
static final homeKey = GlobalKey<_HomePageState>();
|
||||
static final homeKey = GlobalKey<HomePageState>();
|
||||
|
||||
HomePage() : super(key: homeKey);
|
||||
|
||||
@override
|
||||
_HomePageState createState() => _HomePageState();
|
||||
HomePageState createState() => HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
class HomePageState extends State<HomePage> {
|
||||
var _selectedIndex = 0;
|
||||
int get selectedIndex => _selectedIndex;
|
||||
final List<PageShape> _pages = [];
|
||||
@@ -154,7 +154,7 @@ class WebHomePage extends StatelessWidget {
|
||||
// backgroundColor: MyTheme.grayBg,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text("RustDesk" + (isWeb ? " (Beta) " : "")),
|
||||
title: Text("RustDesk${isWeb ? " (Beta) " : ""}"),
|
||||
actions: connectionPage.appBarActions,
|
||||
),
|
||||
body: connectionPage,
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
@@ -20,6 +20,7 @@ import '../../models/input_model.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../utils/image.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
|
||||
final initText = '1' * 1024;
|
||||
|
||||
@@ -59,7 +60,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
Wakelock.enable();
|
||||
WakelockPlus.enable();
|
||||
_physicalFocusNode.requestFocus();
|
||||
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
gFFI.inputModel.listenToMouse(true);
|
||||
@@ -87,7 +88,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
await Wakelock.disable();
|
||||
await WakelockPlus.disable();
|
||||
await keyboardSubscription.cancel();
|
||||
removeSharedStates(widget.id);
|
||||
}
|
||||
@@ -113,6 +114,13 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.ffiModel.pi.version.isNotEmpty) {
|
||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||
}
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
_mobileFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
// update for Scaffold
|
||||
setState(() {});
|
||||
@@ -202,12 +210,12 @@ class _RemotePageState extends State<RemotePage> {
|
||||
_value = initText;
|
||||
setState(() => _showEdit = false);
|
||||
_timer?.cancel();
|
||||
_timer = Timer(Duration(milliseconds: 30), () {
|
||||
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
||||
// show now, and sleep a while to requestFocus to
|
||||
// make sure edit ready, so that keyboard wont show/hide/show/hide happen
|
||||
setState(() => _showEdit = true);
|
||||
_timer?.cancel();
|
||||
_timer = Timer(Duration(milliseconds: 30), () {
|
||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
_mobileFocusNode.requestFocus();
|
||||
@@ -234,7 +242,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
clientClose(sessionId, gFFI.dialogManager);
|
||||
return false;
|
||||
},
|
||||
child: getRawPointerAndKeyBody(Scaffold(
|
||||
child: Scaffold(
|
||||
// workaround for https://github.com/rustdesk/rustdesk/issues/3131
|
||||
floatingActionButtonLocation: keyboardIsVisible
|
||||
? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
|
||||
@@ -280,7 +288,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
: Offstage(),
|
||||
],
|
||||
)),
|
||||
body: Overlay(
|
||||
body: getRawPointerAndKeyBody(Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
return Container(
|
||||
@@ -363,6 +371,10 @@ class _RemotePageState extends State<RemotePage> {
|
||||
? []
|
||||
: gFFI.ffiModel.isPeerAndroid
|
||||
? [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.keyboard),
|
||||
onPressed: openKeyboard),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.build),
|
||||
@@ -481,10 +493,23 @@ class _RemotePageState extends State<RemotePage> {
|
||||
final x = 120.0;
|
||||
final y = size.height;
|
||||
final menus = toolbarControls(context, id, gFFI);
|
||||
getChild(TTextMenu menu) {
|
||||
if (menu.trailingIcon != null) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
menu.child,
|
||||
menu.trailingIcon!,
|
||||
]);
|
||||
} else {
|
||||
return menu.child;
|
||||
}
|
||||
}
|
||||
|
||||
final more = menus
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => PopupMenuItem<int>(child: e.value.child, value: e.key))
|
||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
||||
.toList();
|
||||
() async {
|
||||
var index = await showMenu(
|
||||
@@ -755,14 +780,14 @@ void showOptions(
|
||||
if (image != null) {
|
||||
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
||||
}
|
||||
if (pi.displays.length > 1) {
|
||||
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
|
||||
final cur = pi.currentDisplay;
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < pi.displays.length; ++i) {
|
||||
children.add(InkWell(
|
||||
onTap: () {
|
||||
if (i == cur) return;
|
||||
bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: i);
|
||||
openMonitorInTheSameTab(i, gFFI, pi);
|
||||
gFFI.dialogManager.dismissAll();
|
||||
},
|
||||
child: Ink(
|
||||
@@ -800,6 +825,16 @@ void showOptions(
|
||||
List<TToggleMenu> displayToggles =
|
||||
await toolbarDisplayToggle(context, id, gFFI);
|
||||
|
||||
List<TToggleMenu> privacyModeList = [];
|
||||
// privacy mode
|
||||
final privacyModeState = PrivacyModeState.find(id);
|
||||
if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) {
|
||||
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
||||
if (privacyModeList.length == 1) {
|
||||
displayToggles.add(privacyModeList[0]);
|
||||
}
|
||||
}
|
||||
|
||||
dialogManager.show((setState, close, context) {
|
||||
var viewStyle =
|
||||
(viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
|
||||
@@ -842,10 +877,21 @@ void showOptions(
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
|
||||
Widget privacyModeWidget = Offstage();
|
||||
if (privacyModeList.length > 1) {
|
||||
privacyModeWidget = ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: Text(translate('Privacy mode')),
|
||||
onTap: () => setPrivacyModeDialog(
|
||||
dialogManager, privacyModeList, privacyModeState),
|
||||
);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: displays + radios + toggles),
|
||||
children: displays + radios + toggles + [privacyModeWidget]),
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../consts.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
|
||||
class ScanPage extends StatefulWidget {
|
||||
@@ -60,7 +61,11 @@ class _ScanPageState extends State<ScanPage> {
|
||||
var reader = QRCodeReader();
|
||||
try {
|
||||
var result = reader.decode(bitmap);
|
||||
showServerSettingFromQr(result.text);
|
||||
if (result.text.startsWith(kUniLinksPrefix)) {
|
||||
handleUriLink(uriString: result.text);
|
||||
} else {
|
||||
showServerSettingFromQr(result.text);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('No QR code found');
|
||||
}
|
||||
|
||||
@@ -211,25 +211,18 @@ class ServiceNotRunningNotification extends StatelessWidget {
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: () {
|
||||
if (gFFI.userModel.userName.value.isEmpty && bind.mainGetLocalOption(key: "show-scam-warning") != "N") {
|
||||
_showScamWarning(context, serverModel);
|
||||
if (gFFI.userModel.userName.value.isEmpty &&
|
||||
bind.mainGetLocalOption(key: "show-scam-warning") !=
|
||||
"N") {
|
||||
showScamWarning(context, serverModel);
|
||||
} else {
|
||||
serverModel.toggleService();
|
||||
}
|
||||
},
|
||||
label: Text(translate("Start Service")))
|
||||
label: Text(translate("Start service")))
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
void _showScamWarning(BuildContext context, ServerModel serverModel) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ScamWarningDialog(serverModel: serverModel);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScamWarningDialog extends StatefulWidget {
|
||||
@@ -238,10 +231,10 @@ class ScamWarningDialog extends StatefulWidget {
|
||||
ScamWarningDialog({required this.serverModel});
|
||||
|
||||
@override
|
||||
_ScamWarningDialogState createState() => _ScamWarningDialogState();
|
||||
ScamWarningDialogState createState() => ScamWarningDialogState();
|
||||
}
|
||||
|
||||
class _ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
class ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
int _countdown = 12;
|
||||
bool show_warning = false;
|
||||
late Timer _timer;
|
||||
@@ -277,147 +270,149 @@ class _ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
final isButtonLocked = _countdown > 0;
|
||||
|
||||
return AlertDialog(
|
||||
content: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topRight,
|
||||
end: Alignment.bottomLeft,
|
||||
colors: [
|
||||
Color(0xffe242bc),
|
||||
Color(0xfff4727c),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
padding: EdgeInsets.all(25.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
content: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topRight,
|
||||
end: Alignment.bottomLeft,
|
||||
colors: [
|
||||
Color(0xffe242bc),
|
||||
Color(0xfff4727c),
|
||||
],
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.all(25.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_sharp,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
translate("Warning"),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Center(
|
||||
child: Image.asset('assets/scam.png',
|
||||
width: 180,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
Text(
|
||||
translate("scam_title"),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22.0,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
translate("scam_text1")+"\n\n"
|
||||
+translate("scam_text2")+"\n",
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_sharp,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
translate("Warning"),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
fontSize: 20.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/scam.png',
|
||||
width: 180,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: show_warning,
|
||||
onChanged: (value) {
|
||||
setState((){
|
||||
show_warning = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
Text(
|
||||
translate("Don't show again"),
|
||||
translate("scam_title"),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15.0,
|
||||
fontSize: 22.0,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
Text(
|
||||
"${translate("scam_text1")}\n\n${translate("scam_text2")}\n",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: show_warning,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
show_warning = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
translate("Don't show again"),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: isButtonLocked
|
||||
? null
|
||||
: () {
|
||||
Navigator.of(context).pop();
|
||||
_serverModel.toggleService();
|
||||
if (show_warning) {
|
||||
bind.mainSetLocalOption(
|
||||
key: "show-scam-warning", value: "N");
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueAccent,
|
||||
),
|
||||
child: Text(
|
||||
isButtonLocked
|
||||
? "${translate("I Agree")} (${_countdown}s)"
|
||||
: translate("I Agree"),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13.0,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.blueAccent,
|
||||
),
|
||||
child: Text(
|
||||
translate("Decline"),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13.0,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: isButtonLocked
|
||||
? null
|
||||
: () {
|
||||
Navigator.of(context).pop();
|
||||
_serverModel.toggleService();
|
||||
if (show_warning) {
|
||||
bind.mainSetLocalOption(key: "show-scam-warning", value: "N");
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.blueAccent,
|
||||
),
|
||||
child: Text(
|
||||
isButtonLocked ? translate("I Agree")+" (${_countdown}s)" : translate("I Agree"),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13.0,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15),
|
||||
Container(
|
||||
constraints: BoxConstraints(maxWidth: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.blueAccent,
|
||||
),
|
||||
child: Text(
|
||||
translate("Decline"),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13.0,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)])),
|
||||
contentPadding: EdgeInsets.all(0.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.all(0.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -557,11 +552,17 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
||||
label: Text(translate("Stop service")))
|
||||
.marginOnly(bottom: 8)
|
||||
: SizedBox.shrink(),
|
||||
PermissionRow(translate("Screen Capture"), serverModel.mediaOk,
|
||||
serverModel.toggleService),
|
||||
PermissionRow(
|
||||
translate("Screen Capture"),
|
||||
serverModel.mediaOk,
|
||||
!serverModel.mediaOk &&
|
||||
gFFI.userModel.userName.value.isEmpty &&
|
||||
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
|
||||
? () => showScamWarning(context, serverModel)
|
||||
: serverModel.toggleService),
|
||||
PermissionRow(translate("Input Control"), serverModel.inputOk,
|
||||
serverModel.toggleInput),
|
||||
PermissionRow(translate("Transfer File"), serverModel.fileOk,
|
||||
PermissionRow(translate("Transfer file"), serverModel.fileOk,
|
||||
serverModel.toggleFile),
|
||||
hasAudioPermission
|
||||
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
|
||||
@@ -801,3 +802,12 @@ void androidChannelInit() {
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
void showScamWarning(BuildContext context, ServerModel serverModel) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ScamWarningDialog(serverModel: serverModel);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,7 +221,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Deny LAN Discovery')),
|
||||
title: Text(translate('enable-2fa-title')),
|
||||
initialValue: bind.mainHasValid2FaSync(),
|
||||
onToggle: (_) async {
|
||||
update() async {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
change2fa(callback: update);
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Deny LAN discovery')),
|
||||
initialValue: _denyLANDiscovery,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
@@ -270,7 +281,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Enable Recording Session')),
|
||||
title: Text(translate('Enable recording session')),
|
||||
initialValue: _enableRecordSession,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
@@ -407,7 +418,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
enhancementsTiles.add(SettingsTile.switchTile(
|
||||
initialValue: _enableStartOnBoot,
|
||||
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text("${translate('Start on Boot')} (beta)"),
|
||||
Text("${translate('Start on boot')} (beta)"),
|
||||
Text(
|
||||
'* ${translate('Start the screen sharing service on boot, requires special permissions')}',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
@@ -708,7 +719,7 @@ class ScanButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _DisplayPage extends StatefulWidget {
|
||||
const _DisplayPage({super.key});
|
||||
const _DisplayPage();
|
||||
|
||||
@override
|
||||
State<_DisplayPage> createState() => __DisplayPageState();
|
||||
@@ -789,21 +800,14 @@ class __DisplayPageState extends State<_DisplayPage> {
|
||||
),
|
||||
SettingsSection(
|
||||
title: Text(translate('Other Default Options')),
|
||||
tiles: [
|
||||
otherRow('Show remote cursor', 'show_remote_cursor'),
|
||||
otherRow('Show quality monitor', 'show_quality_monitor'),
|
||||
otherRow('Mute', 'disable_audio'),
|
||||
otherRow('Disable clipboard', 'disable_clipboard'),
|
||||
otherRow('Lock after session end', 'lock_after_session_end'),
|
||||
otherRow('Privacy mode', 'privacy_mode'),
|
||||
otherRow('Touch mode', 'touch-mode'),
|
||||
],
|
||||
tiles:
|
||||
otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList(),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
otherRow(String label, String key) {
|
||||
SettingsTile otherRow(String label, String key) {
|
||||
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
||||
return SettingsTile.switchTile(
|
||||
initialValue: value,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
@@ -170,7 +171,7 @@ void showServerSettingsWithValue(
|
||||
isInProgress = true;
|
||||
});
|
||||
bool ret = await setServerConfig(
|
||||
controllers,
|
||||
null,
|
||||
errMsgs,
|
||||
ServerConfig(
|
||||
idServer: idCtrl.text.trim(),
|
||||
@@ -259,6 +260,30 @@ void showServerSettingsWithValue(
|
||||
});
|
||||
}
|
||||
|
||||
void setPrivacyModeDialog(
|
||||
OverlayDialogManager dialogManager,
|
||||
List<TToggleMenu> privacyModeList,
|
||||
RxString privacyModeState,
|
||||
) async {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Privacy mode')),
|
||||
content: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: privacyModeList
|
||||
.map((value) => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: value.child,
|
||||
value: value.value,
|
||||
onChanged: value.onChanged,
|
||||
))
|
||||
.toList()),
|
||||
);
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
Future<String?> validateAsync(String value) async {
|
||||
value = value.trim();
|
||||
if (value.isEmpty) {
|
||||
|
||||
@@ -7,30 +7,27 @@ class GestureIcons {
|
||||
|
||||
GestureIcons._();
|
||||
|
||||
static const IconData icon_mouse = IconData(0xe65c, fontFamily: _family);
|
||||
static const IconData icon_Tablet_Touch =
|
||||
IconData(0xe9ce, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_drag =
|
||||
static const IconData iconMouse = IconData(0xe65c, fontFamily: _family);
|
||||
static const IconData iconTabletTouch = IconData(0xe9ce, fontFamily: _family);
|
||||
static const IconData iconGestureFDrag =
|
||||
IconData(0xe686, fontFamily: _family);
|
||||
static const IconData icon_Mobile_Touch =
|
||||
IconData(0xe9cd, fontFamily: _family);
|
||||
static const IconData icon_gesture_press =
|
||||
static const IconData iconMobileTouch = IconData(0xe9cd, fontFamily: _family);
|
||||
static const IconData iconGesturePress =
|
||||
IconData(0xe66c, fontFamily: _family);
|
||||
static const IconData icon_gesture_tap =
|
||||
IconData(0xe66f, fontFamily: _family);
|
||||
static const IconData icon_gesture_pinch =
|
||||
static const IconData iconGestureTap = IconData(0xe66f, fontFamily: _family);
|
||||
static const IconData iconGesturePinch =
|
||||
IconData(0xe66a, fontFamily: _family);
|
||||
static const IconData icon_gesture_press_hold =
|
||||
static const IconData iconGesturePressHold =
|
||||
IconData(0xe66b, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_drag_up_down_ =
|
||||
static const IconData iconGestureFDragUpDown_ =
|
||||
IconData(0xe685, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_tap_ =
|
||||
static const IconData iconGestureFTap_ =
|
||||
IconData(0xe68e, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_swipe_right =
|
||||
static const IconData iconGestureFSwipeRight =
|
||||
IconData(0xe68f, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_double_tap =
|
||||
static const IconData iconGestureFdoubleTap =
|
||||
IconData(0xe691, fontFamily: _family);
|
||||
static const IconData icon_gesture_f_three_fingers =
|
||||
static const IconData iconGestureFThreeFingers =
|
||||
IconData(0xe687, fontFamily: _family);
|
||||
}
|
||||
|
||||
@@ -106,64 +103,64 @@ class _GestureHelpState extends State<GestureHelp> {
|
||||
? [
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_Mobile_Touch,
|
||||
GestureIcons.iconMobileTouch,
|
||||
translate("One-Finger Tap"),
|
||||
translate("Left Mouse")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_press_hold,
|
||||
GestureIcons.iconGesturePressHold,
|
||||
translate("One-Long Tap"),
|
||||
translate("Right Mouse")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_swipe_right,
|
||||
GestureIcons.iconGestureFSwipeRight,
|
||||
translate("One-Finger Move"),
|
||||
translate("Mouse Drag")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_three_fingers,
|
||||
GestureIcons.iconGestureFThreeFingers,
|
||||
translate("Three-Finger vertically"),
|
||||
translate("Mouse Wheel")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_drag,
|
||||
GestureIcons.iconGestureFDrag,
|
||||
translate("Two-Finger Move"),
|
||||
translate("Canvas Move")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_pinch,
|
||||
GestureIcons.iconGesturePinch,
|
||||
translate("Pinch to Zoom"),
|
||||
translate("Canvas Zoom")),
|
||||
]
|
||||
: [
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_Mobile_Touch,
|
||||
GestureIcons.iconMobileTouch,
|
||||
translate("One-Finger Tap"),
|
||||
translate("Left Mouse")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_press_hold,
|
||||
GestureIcons.iconGesturePressHold,
|
||||
translate("One-Long Tap"),
|
||||
translate("Right Mouse")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_swipe_right,
|
||||
GestureIcons.iconGestureFSwipeRight,
|
||||
translate("Double Tap & Move"),
|
||||
translate("Mouse Drag")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_three_fingers,
|
||||
GestureIcons.iconGestureFThreeFingers,
|
||||
translate("Three-Finger vertically"),
|
||||
translate("Mouse Wheel")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_f_drag,
|
||||
GestureIcons.iconGestureFDrag,
|
||||
translate("Two-Finger Move"),
|
||||
translate("Canvas Move")),
|
||||
GestureInfo(
|
||||
width,
|
||||
GestureIcons.icon_gesture_pinch,
|
||||
GestureIcons.iconGesturePinch,
|
||||
translate("Pinch to Zoom"),
|
||||
translate("Canvas Zoom")),
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user