mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-19 07:09:10 +08:00
Compare commits
1201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
588103c6dc | ||
|
|
2ce9b108ed | ||
|
|
93e3107881 | ||
|
|
468bdd6cc6 | ||
|
|
d5c5825ffd | ||
|
|
fe4094777f | ||
|
|
f13ef48cec | ||
|
|
a23822074e | ||
|
|
3d17bf4990 | ||
|
|
fd67be4a16 | ||
|
|
e6edf39305 | ||
|
|
34d2c62781 | ||
|
|
bd0a33e467 | ||
|
|
b8d36b6558 | ||
|
|
f38d89aaee | ||
|
|
773b9d6645 | ||
|
|
dea99ffb3a | ||
|
|
3251045e22 | ||
|
|
dc58c85e30 | ||
|
|
5a2a94d2cc | ||
|
|
f330953f4f | ||
|
|
8d4c86fe7f | ||
|
|
f8c2713c5b | ||
|
|
082a66b282 | ||
|
|
743b0ce8ce | ||
|
|
9d9b67aca5 | ||
|
|
d60b5a6ca0 | ||
|
|
e0ed6ee986 | ||
|
|
d3f0c80e94 | ||
|
|
b32ff87c6e | ||
|
|
b91b49229a | ||
|
|
afc8bb71dc | ||
|
|
4f86169f7f | ||
|
|
2cf43042e6 | ||
|
|
734fb8d6f7 | ||
|
|
3c7f6d3127 | ||
|
|
b99c540210 | ||
|
|
84dab0e96f | ||
|
|
458a88fb89 | ||
|
|
8a70932cd6 | ||
|
|
edfae98a01 | ||
|
|
d61c99b105 | ||
|
|
30a11bfe0a | ||
|
|
9d2bdfefb1 | ||
|
|
34b93c6f83 | ||
|
|
152d0ce74b | ||
|
|
32a3bcdc4f | ||
|
|
314c93b210 | ||
|
|
b64f6271e2 | ||
|
|
ac044c4049 | ||
|
|
0973f51df9 | ||
|
|
02b046bdbf | ||
|
|
02c274aeb6 | ||
|
|
ab6a6ca17d | ||
|
|
28d38cd71d | ||
|
|
b487f297b8 | ||
|
|
64654ee7cf | ||
|
|
1c99eb5500 | ||
|
|
74dd0c8fa0 | ||
|
|
d26fea41ee | ||
|
|
bc211c8031 | ||
|
|
d4cb7d68c5 | ||
|
|
608d7d55d5 | ||
|
|
4a49fbe4a6 | ||
|
|
f760e21ff8 | ||
|
|
251e1a3487 | ||
|
|
b990ff3782 | ||
|
|
c5426b0fbc | ||
|
|
8b710f62c8 | ||
|
|
0707e791e8 | ||
|
|
a07392e6b8 | ||
|
|
9125a68f81 | ||
|
|
9ee77a9b92 | ||
|
|
304e0e465d | ||
|
|
5d2bb9c995 | ||
|
|
ddc172bdfa | ||
|
|
06c7bc137f | ||
|
|
9e4cc91a14 | ||
|
|
0a28d09ff8 | ||
|
|
ab89d84a8f | ||
|
|
0aa98eac6d | ||
|
|
d4aa2b7ce4 | ||
|
|
35b4535ebc | ||
|
|
f0be80c253 | ||
|
|
a79a9f697b | ||
|
|
6082bb2754 | ||
|
|
4e6a43288e | ||
|
|
72a1f1161e | ||
|
|
912f5265f1 | ||
|
|
5eb2c31207 | ||
|
|
062c8d582c | ||
|
|
68b07505ab | ||
|
|
d3efcd4223 | ||
|
|
a277b022ff | ||
|
|
740c5358ab | ||
|
|
7978e0301d | ||
|
|
0f070b0108 | ||
|
|
6f0cb3b8c2 | ||
|
|
d0ef52e418 | ||
|
|
69277dd16b | ||
|
|
faf97c770c | ||
|
|
78088360ca | ||
|
|
5cfd1701fb | ||
|
|
a4bd23c9de | ||
|
|
12c1337b7b | ||
|
|
040253b319 | ||
|
|
44fa83d080 | ||
|
|
4f7e10bac6 | ||
|
|
d1fdcf1b16 | ||
|
|
4c12b83068 | ||
|
|
f86c88b3d8 | ||
|
|
697dd87383 | ||
|
|
0b8cccd8be | ||
|
|
32dbc0c8fb | ||
|
|
bae4a2c710 | ||
|
|
711ed28846 | ||
|
|
e6c5064ce5 | ||
|
|
0f5f9f6524 | ||
|
|
ce7867c1c0 | ||
|
|
415d2c5c60 | ||
|
|
a289eae07c | ||
|
|
1c9b456456 | ||
|
|
eba19e67ff | ||
|
|
adc5a7be51 | ||
|
|
26d23d588a | ||
|
|
f0450db203 | ||
|
|
3a75947553 | ||
|
|
c565849062 | ||
|
|
40e8f0d307 | ||
|
|
129f6c869b | ||
|
|
924aa515c6 | ||
|
|
c51771c854 | ||
|
|
c8b9031996 | ||
|
|
4da584055d | ||
|
|
bd22b01370 | ||
|
|
b35b48086a | ||
|
|
445e9ac285 | ||
|
|
7a3e1fe648 | ||
|
|
dfa9519d58 | ||
|
|
cc6f919080 | ||
|
|
2cdaca0fa3 | ||
|
|
6159449eba | ||
|
|
6088920f8d | ||
|
|
e8187588c1 | ||
|
|
289076aa70 | ||
|
|
547da31095 | ||
|
|
1bf4ef1f46 | ||
|
|
1212d9fa2d | ||
|
|
8c8a643cce | ||
|
|
675ffe0381 | ||
|
|
844caf8c15 | ||
|
|
0f6d28def7 | ||
|
|
0d3243e6dd | ||
|
|
53d11e99d7 | ||
|
|
defb3e6c73 | ||
|
|
ae8dfe84a0 | ||
|
|
5e920f0fd0 | ||
|
|
1a0814b201 | ||
|
|
ace98d98ad | ||
|
|
09083b3afa | ||
|
|
36e11c61a9 | ||
|
|
55187e9243 | ||
|
|
ae1c1a56e6 | ||
|
|
cdd58e77eb | ||
|
|
ce924cc0d3 | ||
|
|
498b8ba3d6 | ||
|
|
af610b2408 | ||
|
|
6cdbcfc082 | ||
|
|
9c7f51bc76 | ||
|
|
65683cc3e6 | ||
|
|
eb1ef0969c | ||
|
|
29b01e9cef | ||
|
|
cde7620eda | ||
|
|
844b853074 | ||
|
|
97f02ed25e | ||
|
|
22c84bbbd1 | ||
|
|
227f154ee7 | ||
|
|
59d7bf1e86 | ||
|
|
38fcf4e039 | ||
|
|
4b3b31147e | ||
|
|
e6d4067f48 | ||
|
|
507de628c9 | ||
|
|
2591d4f044 | ||
|
|
9bcd0d1b03 | ||
|
|
5555ba6b2f | ||
|
|
28b6bc186f | ||
|
|
00d38260e1 | ||
|
|
e06f456bbd | ||
|
|
cc860b2906 | ||
|
|
839e8180e0 | ||
|
|
83aba804d0 | ||
|
|
560c1effe8 | ||
|
|
e7353be0cd | ||
|
|
ba832362a7 | ||
|
|
9ea09c1515 | ||
|
|
3a97b63e95 | ||
|
|
dec3cde9b3 | ||
|
|
c4d0b02478 | ||
|
|
306dd77b81 | ||
|
|
fd62751cb8 | ||
|
|
b0edfb8f70 | ||
|
|
334526026c | ||
|
|
b5414ec002 | ||
|
|
4eca8b9447 | ||
|
|
1ebc726acd | ||
|
|
d563372a91 | ||
|
|
4a745d82f6 | ||
|
|
2f5f701dc7 | ||
|
|
60a0099ba0 | ||
|
|
30a7847100 | ||
|
|
1e822fa135 | ||
|
|
f6261883e8 | ||
|
|
3365844def | ||
|
|
769bbf1e1c | ||
|
|
81b999cfbe | ||
|
|
9959217cc3 | ||
|
|
3e6938bec6 | ||
|
|
4459406578 | ||
|
|
beb1084e87 | ||
|
|
d4184fd865 | ||
|
|
ffc73f86a0 | ||
|
|
c74bdcdfdb | ||
|
|
6d8b5b289f | ||
|
|
1d6873f622 | ||
|
|
7c55e3266b | ||
|
|
ce5151032e | ||
|
|
ba88bc9e8b | ||
|
|
e0095aebda | ||
|
|
664a3e186e | ||
|
|
e4f7e126e5 | ||
|
|
49989e34e4 | ||
|
|
75a14fea23 | ||
|
|
f535406962 | ||
|
|
f3f3bb538f | ||
|
|
8fefd34c15 | ||
|
|
d98f947824 | ||
|
|
5f52ce2c1b | ||
|
|
1d799483d7 | ||
|
|
3db55a718c | ||
|
|
a516f01feb | ||
|
|
2e314bf032 | ||
|
|
b93d4ce3fc | ||
|
|
21bcfd173d | ||
|
|
3d5262c36f | ||
|
|
cfd801c5d6 | ||
|
|
216a72592d | ||
|
|
ddd3401bd7 | ||
|
|
47139edd81 | ||
|
|
c6e3f60a6b | ||
|
|
88a99211f3 | ||
|
|
d08c335fdf | ||
|
|
e5ec6957fe | ||
|
|
e20f5dd001 | ||
|
|
e1a6ccc100 | ||
|
|
cc288272d3 | ||
|
|
49ce4edb8a | ||
|
|
29c3b29bda | ||
|
|
8a8f708c3e | ||
|
|
c5038b1a78 | ||
|
|
f4c038ea93 | ||
|
|
d9ea717056 | ||
|
|
40af9dc78b | ||
|
|
81fc22a156 | ||
|
|
2e7bd26e4c | ||
|
|
179b562472 | ||
|
|
ab246fdcbf | ||
|
|
d65d3b7326 | ||
|
|
9f9a22ec63 | ||
|
|
a8f1a66043 | ||
|
|
0b3e7bf33e | ||
|
|
c358399eca | ||
|
|
cacca7295c | ||
|
|
d2e98cc620 | ||
|
|
2e81bcb447 | ||
|
|
cbca0eb340 | ||
|
|
9380f33d7c | ||
|
|
519539ed0a | ||
|
|
1f2a75fbd8 | ||
|
|
51055a7e5b | ||
|
|
13effe7f14 | ||
|
|
943f96ef8c | ||
|
|
260a82ee5c | ||
|
|
a2792d1527 | ||
|
|
2922ebe22a | ||
|
|
1e6944b380 | ||
|
|
993862c103 | ||
|
|
c8cd564e69 | ||
|
|
a4cd64f0d5 | ||
|
|
f0ca4b9fee | ||
|
|
aa3402b44a | ||
|
|
26ebd0deb9 | ||
|
|
4150036589 | ||
|
|
7a1157f1b0 | ||
|
|
3bd34bf0b9 | ||
|
|
5f29016861 | ||
|
|
e40243b55d | ||
|
|
dbbbd08934 | ||
|
|
29e12b84a9 | ||
|
|
04c0f66ca9 | ||
|
|
ec28567362 | ||
|
|
d4377a13c5 | ||
|
|
39e713838f | ||
|
|
75a4671bda | ||
|
|
827efabbc0 | ||
|
|
532fe6aefb | ||
|
|
ae339f039d | ||
|
|
bf390611ab | ||
|
|
e3f6829d02 | ||
|
|
832002a10f | ||
|
|
d335cdbb0c | ||
|
|
6a5d5875c8 | ||
|
|
cf06d1028f | ||
|
|
fd178a7b6c | ||
|
|
f3a2733d75 | ||
|
|
55de573a01 | ||
|
|
40239a1c41 | ||
|
|
c68ce7dd84 | ||
|
|
690a2c8399 | ||
|
|
4b4fd94f3e | ||
|
|
5abe42f66c | ||
|
|
48aec6484c | ||
|
|
a946d4d0c9 | ||
|
|
24f4b94082 | ||
|
|
aa1e122532 | ||
|
|
d400999b9c | ||
|
|
1d416f6626 | ||
|
|
9d9741f18e | ||
|
|
50aa8e12ad | ||
|
|
5931af460e | ||
|
|
fc607d6789 | ||
|
|
529e70910d | ||
|
|
f300d797e2 | ||
|
|
e3cce2824d | ||
|
|
f34b8411a7 | ||
|
|
8745fcbb6a | ||
|
|
2a0fd55af7 | ||
|
|
da70cbcdda | ||
|
|
921b64e1e0 | ||
|
|
c0de0aa108 | ||
|
|
715d475f49 | ||
|
|
e3f09b3ec6 | ||
|
|
0a5fafb84f | ||
|
|
4e084c5ee0 | ||
|
|
d1fe617670 | ||
|
|
7744bdbbe0 | ||
|
|
e1329c8157 | ||
|
|
f31e60af5b | ||
|
|
ed18e3c786 | ||
|
|
579e0fac36 | ||
|
|
92752765ba | ||
|
|
071f51cf6f | ||
|
|
dde3cce120 | ||
|
|
bb1b9858d5 | ||
|
|
a31c27be73 | ||
|
|
85ae3916cb | ||
|
|
cc9b7e64eb | ||
|
|
5e22a49e49 | ||
|
|
07cf1b4db5 | ||
|
|
f6ab5cdcb2 | ||
|
|
b477aded0b | ||
|
|
65318efd67 | ||
|
|
dbd195a46e | ||
|
|
0651ad492f | ||
|
|
a3c5adb1f4 | ||
|
|
a771abcdc2 | ||
|
|
b8b3a089f3 | ||
|
|
8f00067266 | ||
|
|
83bf067d18 | ||
|
|
1729ee337f | ||
|
|
57834840b8 | ||
|
|
99d7b62d79 | ||
|
|
6625aca994 | ||
|
|
ce56be6507 | ||
|
|
fd69b14623 | ||
|
|
7521bbe15f | ||
|
|
3c6ddd7403 | ||
|
|
6820e2f4c7 | ||
|
|
77f3ebaf1a | ||
|
|
e7e244d4f2 | ||
|
|
f4c40d733e | ||
|
|
049c334db3 | ||
|
|
6197832317 | ||
|
|
2fd53f9825 | ||
|
|
ae16b8975b | ||
|
|
171177c76f | ||
|
|
ade1d8c0c7 | ||
|
|
9a194f0850 | ||
|
|
76d5a8b205 | ||
|
|
bc6ce6c7ee | ||
|
|
025cdfa25b | ||
|
|
2f432e941d | ||
|
|
96edca8f74 | ||
|
|
51b250435d | ||
|
|
5a2121501d | ||
|
|
877b3e2ce5 | ||
|
|
421ddc0016 | ||
|
|
2662abc5a3 | ||
|
|
b3e1c8a907 | ||
|
|
2266fde26f | ||
|
|
e58e75eea9 | ||
|
|
eafebdba21 | ||
|
|
7bf5e69444 | ||
|
|
cb0dc46d08 | ||
|
|
9b8209b61b | ||
|
|
b6ba9978e3 | ||
|
|
0d1d7a9b87 | ||
|
|
508dd5b383 | ||
|
|
31a1b7a80b | ||
|
|
ba43424781 | ||
|
|
f899b2a962 | ||
|
|
2dd3d8c11e | ||
|
|
6eea425280 | ||
|
|
5e7d4fd2d6 | ||
|
|
d9fba50606 | ||
|
|
b6035fbbdf | ||
|
|
e67b694f06 | ||
|
|
2333ee2c07 | ||
|
|
61ccc2152e | ||
|
|
f6aca4ca8e | ||
|
|
1707987a7b | ||
|
|
85604dee79 | ||
|
|
a12969be30 | ||
|
|
9f91eada89 | ||
|
|
cb6a6aa42a | ||
|
|
4fec8abad4 | ||
|
|
35571dc8d7 | ||
|
|
a103b83647 | ||
|
|
e1e4bf599b | ||
|
|
cba8aaa410 | ||
|
|
8ced4ddaa2 | ||
|
|
15404ecab4 | ||
|
|
97772f9ac5 | ||
|
|
764fbe2c9d | ||
|
|
e03344d85b | ||
|
|
0e98a51775 | ||
|
|
0a1d3c4afb | ||
|
|
fd9b5f3c57 | ||
|
|
73f6afd4c0 | ||
|
|
50dd2b3aad | ||
|
|
c0e9445602 | ||
|
|
541d9c6b86 | ||
|
|
8c91e5c5ca | ||
|
|
19d1605d8c | ||
|
|
0faf82f109 | ||
|
|
ee5314de20 | ||
|
|
7e8d3bd2ac | ||
|
|
9750e1409c | ||
|
|
67d4e061fb | ||
|
|
f67f2be0cb | ||
|
|
aa42bf548e | ||
|
|
d679e8fa7d | ||
|
|
1357ef5d6f | ||
|
|
30a5d1e0e1 | ||
|
|
9f0985c842 | ||
|
|
be06c0d738 | ||
|
|
f0f50f0f03 | ||
|
|
1850d32f49 | ||
|
|
3999d498be | ||
|
|
bbdce8d57b | ||
|
|
baf70da2fe | ||
|
|
b967d496cc | ||
|
|
2aef79688b | ||
|
|
0451a1c45f | ||
|
|
f7e9057a39 | ||
|
|
d73e0e1e5a | ||
|
|
39dbd89287 | ||
|
|
c04f460bbd | ||
|
|
79a1f888d6 | ||
|
|
57d1b1ecc4 | ||
|
|
8a4a2b5732 | ||
|
|
51a60a7eed | ||
|
|
b26acde450 | ||
|
|
2de81045ea | ||
|
|
a72a8906b0 | ||
|
|
614086a216 | ||
|
|
2ffc2ad85b | ||
|
|
eef091d4e8 | ||
|
|
97f26f880b | ||
|
|
22c6f5e589 | ||
|
|
b828768fa9 | ||
|
|
31e7b6acf1 | ||
|
|
14b505130b | ||
|
|
22f3425ace | ||
|
|
4723d6a830 | ||
|
|
c4f3c0f133 | ||
|
|
de375c91bb | ||
|
|
d3454f07d3 | ||
|
|
cf8ef2533a | ||
|
|
6ad662260e | ||
|
|
2b54a553c7 | ||
|
|
85ded0a3e5 | ||
|
|
5c16a8302e | ||
|
|
04c175c62e | ||
|
|
2be05608d8 | ||
|
|
c3c99ba107 | ||
|
|
a81d6468cc | ||
|
|
48464835f5 | ||
|
|
edc5d86ee7 | ||
|
|
e9c8ba5393 | ||
|
|
a72bc0fb28 | ||
|
|
5a8c8cbf7c | ||
|
|
b68d7a3054 | ||
|
|
9e931a6f04 | ||
|
|
f0587796e2 | ||
|
|
875ac28ab5 | ||
|
|
6821bef5e5 | ||
|
|
930561f431 | ||
|
|
bc672b3367 | ||
|
|
e283d33f28 | ||
|
|
901505e8be | ||
|
|
a4565bf0da | ||
|
|
092e4089c7 | ||
|
|
188f85b042 | ||
|
|
72c96f22b6 | ||
|
|
0143eaf601 | ||
|
|
09466680d3 | ||
|
|
eec879a801 | ||
|
|
3f11d9cdb6 | ||
|
|
8512c2b2b0 | ||
|
|
e2a7e38a39 | ||
|
|
3a0ece1447 | ||
|
|
d0a54a6cc6 | ||
|
|
bed214bd37 | ||
|
|
5f31211db3 | ||
|
|
29b8875c1c | ||
|
|
3367c541b2 | ||
|
|
30afe4f779 | ||
|
|
d18e95703e | ||
|
|
9adc083def | ||
|
|
0dc664474a | ||
|
|
5e8fe239fa | ||
|
|
7a3100a87c | ||
|
|
8a1acedae5 | ||
|
|
f5bcc17636 | ||
|
|
883c630206 | ||
|
|
a95a6ab733 | ||
|
|
46605fab1b | ||
|
|
9d26fec631 | ||
|
|
294a6ce9bc | ||
|
|
183ea47ba4 | ||
|
|
06e04143a8 | ||
|
|
a532b36e28 | ||
|
|
c873b69662 | ||
|
|
b30f84623b | ||
|
|
888e993534 | ||
|
|
1d59a7fe5f | ||
|
|
2c027cdcf5 | ||
|
|
fe513dd967 | ||
|
|
d652b99d5b | ||
|
|
c2716c2509 | ||
|
|
821f7245b0 | ||
|
|
0ea88ce6ff | ||
|
|
21f41e98a0 | ||
|
|
282ea02ebf | ||
|
|
170200fa49 | ||
|
|
d8cee6507d | ||
|
|
2391b18046 | ||
|
|
b5a7165015 | ||
|
|
ef4d84657b | ||
|
|
011647511c | ||
|
|
e2d217a138 | ||
|
|
f07936a911 | ||
|
|
0bb4d43e9e | ||
|
|
6f74080a2d | ||
|
|
8a370e640a | ||
|
|
d007408061 | ||
|
|
02572e9032 | ||
|
|
af66d2a73b | ||
|
|
eb5ab4d7d9 | ||
|
|
c02b4f994a | ||
|
|
d093fdc256 | ||
|
|
2af799f46e | ||
|
|
3c7e24c605 | ||
|
|
7d961d895b | ||
|
|
53dbc2fa6f | ||
|
|
024220e58a | ||
|
|
8621b93436 | ||
|
|
ac88121c4a | ||
|
|
90df80ed78 | ||
|
|
d4f3a87276 | ||
|
|
48efdcf1f0 | ||
|
|
0511cdbb21 | ||
|
|
8747b9847f | ||
|
|
92d0fe1c3f | ||
|
|
a9015bcf70 | ||
|
|
f8f2686267 | ||
|
|
c2bd1b8965 | ||
|
|
4eeee5b7ee | ||
|
|
dfc224ec01 | ||
|
|
86ff768241 | ||
|
|
94addb162b | ||
|
|
bea65f8739 | ||
|
|
92f570831d | ||
|
|
9349210a87 | ||
|
|
95f4274eca | ||
|
|
a6febb2816 | ||
|
|
e294dafe7c | ||
|
|
d00582e929 | ||
|
|
6d2e985593 | ||
|
|
182e8c4ac0 | ||
|
|
40019b80f6 | ||
|
|
2f40b9dc04 | ||
|
|
8602b036bd | ||
|
|
51db8e706d | ||
|
|
a0dc38f749 | ||
|
|
625b610cfd | ||
|
|
62a8349739 | ||
|
|
0ab500c27c | ||
|
|
285e974d1a | ||
|
|
e71d86c124 | ||
|
|
14343e89d4 | ||
|
|
3f2dfa521c | ||
|
|
cd73368cb9 | ||
|
|
84b5cd70ed | ||
|
|
01672bc697 | ||
|
|
763174657b | ||
|
|
15fa80fb26 | ||
|
|
d537e2563d | ||
|
|
1719e478e3 | ||
|
|
1f129e6ef3 | ||
|
|
25d0ced8ba | ||
|
|
2116fec20b | ||
|
|
1252f45506 | ||
|
|
1f4c62e480 | ||
|
|
bd334769fa | ||
|
|
750368af7b | ||
|
|
2fb35c3596 | ||
|
|
5114a9d369 | ||
|
|
4b6ba7938f | ||
|
|
1e400d2a64 | ||
|
|
967e63266f | ||
|
|
f9b0a88213 | ||
|
|
d67afa49b4 | ||
|
|
1fd170b089 | ||
|
|
a632718e80 | ||
|
|
9f72d05749 | ||
|
|
c062813c6d | ||
|
|
3ae1638125 | ||
|
|
96aff38862 | ||
|
|
ed3fb1efa4 | ||
|
|
d689bbf38e | ||
|
|
c1bbdaf9ae | ||
|
|
ab9e1013b2 | ||
|
|
e1140b1bea | ||
|
|
cfd27c8d87 | ||
|
|
a18947eed2 | ||
|
|
f8592e0d5b | ||
|
|
5bfdf05ff2 | ||
|
|
9e851542ec | ||
|
|
e79946b4e4 | ||
|
|
aed212d8f8 | ||
|
|
c5d3c7f390 | ||
|
|
b047730830 | ||
|
|
9c7d4ef1f7 | ||
|
|
12d3c59172 | ||
|
|
ef06b7d5d0 | ||
|
|
f17e17a6b9 | ||
|
|
faf363cfd2 | ||
|
|
dbbd9179b7 | ||
|
|
49f848a453 | ||
|
|
ef56aea74f | ||
|
|
cb5fa85ac2 | ||
|
|
11bdd3cfcd | ||
|
|
f0dcc91907 | ||
|
|
c1c2d26ec7 | ||
|
|
93133b9a6c | ||
|
|
245f08055f | ||
|
|
00ddd63372 | ||
|
|
1765c7bbf4 | ||
|
|
65edd55516 | ||
|
|
4947cf8718 | ||
|
|
65dd2b8993 | ||
|
|
ef82cfa034 | ||
|
|
1a69d525af | ||
|
|
307827be3c | ||
|
|
40cb59336f | ||
|
|
a9e0ea8520 | ||
|
|
baeee642dd | ||
|
|
416efe9fd3 | ||
|
|
8b5ac390d1 | ||
|
|
212e8e7559 | ||
|
|
41a20b50ea | ||
|
|
3742b51d58 | ||
|
|
1a21dff5d4 | ||
|
|
bbf7d9e08a | ||
|
|
ffed29e632 | ||
|
|
0f6538c1a7 | ||
|
|
ff2e055a5a | ||
|
|
cdf97f8717 | ||
|
|
b2af79a3c5 | ||
|
|
74cc5abd09 | ||
|
|
32c4712d5e | ||
|
|
3244395bfb | ||
|
|
1cb0e1ce7b | ||
|
|
42394fcbdd | ||
|
|
0b32e741f7 | ||
|
|
80c5d59916 | ||
|
|
e95823f543 | ||
|
|
06fe972683 | ||
|
|
3057396c02 | ||
|
|
7db9543fee | ||
|
|
58d86acf0d | ||
|
|
859020583d | ||
|
|
0cab620ba5 | ||
|
|
4338fcc51a | ||
|
|
5f6f1e8d36 | ||
|
|
a91f244f35 | ||
|
|
82bf04da81 | ||
|
|
a679e4a5e3 | ||
|
|
3c79404534 | ||
|
|
ba707d1149 | ||
|
|
93d88f30b4 | ||
|
|
f5bc136b07 | ||
|
|
ae69cbb207 | ||
|
|
39e3da1eb0 | ||
|
|
c1322b47c3 | ||
|
|
67f83bd5dd | ||
|
|
e424d01f3d | ||
|
|
a424830893 | ||
|
|
3c5810cc01 | ||
|
|
30bd4e1cef | ||
|
|
7956953669 | ||
|
|
dcba4615a2 | ||
|
|
0bf9de8256 | ||
|
|
77f1c7e74c | ||
|
|
27478946ea | ||
|
|
1f25a8af86 | ||
|
|
d75caad71f | ||
|
|
adf0226641 | ||
|
|
137f58a84a | ||
|
|
7c45a68870 | ||
|
|
99edab4b61 | ||
|
|
e50b72622c | ||
|
|
60dc40f47f | ||
|
|
841c331981 | ||
|
|
4eafa5a585 | ||
|
|
5a740e891e | ||
|
|
1fcc7001bd | ||
|
|
e57854422a | ||
|
|
8c39979848 | ||
|
|
2c38648e39 | ||
|
|
97aa739d69 | ||
|
|
b0042f29fb | ||
|
|
e3ca82945f | ||
|
|
bf6a3a7067 | ||
|
|
d25670c79a | ||
|
|
32b26e4ad3 | ||
|
|
818439db48 | ||
|
|
e23a9da1a8 | ||
|
|
37ebac2a9e | ||
|
|
46bf552afc | ||
|
|
70151e3dd8 | ||
|
|
e933f0baf2 | ||
|
|
f2a612c3d9 | ||
|
|
4a648f0068 | ||
|
|
5b52742cf7 | ||
|
|
237d234277 | ||
|
|
ed0cba281f | ||
|
|
2e0eaed322 | ||
|
|
e2a6d66805 | ||
|
|
8d6de9ca59 | ||
|
|
db108d964b | ||
|
|
f016d453fa | ||
|
|
60ea8d2c2b | ||
|
|
12ff1319f1 | ||
|
|
f224d8872e | ||
|
|
5cf2d5f062 | ||
|
|
92dd0ee1dd | ||
|
|
70c20fc76f | ||
|
|
07e0b5ac10 | ||
|
|
12f7fc3d33 | ||
|
|
60f47cb549 | ||
|
|
d33fa3f073 | ||
|
|
2e4fafcf46 | ||
|
|
ab451b9056 | ||
|
|
bc875a35b0 | ||
|
|
8e12a34634 | ||
|
|
77204127f2 | ||
|
|
65c2ccdc93 | ||
|
|
964d4f1f87 | ||
|
|
f559e9c74a | ||
|
|
610009528b | ||
|
|
0f10a88b23 | ||
|
|
60049c8cc5 | ||
|
|
50aa5880de | ||
|
|
47143318ba | ||
|
|
c27791a9ac | ||
|
|
b19d732a3a | ||
|
|
cd3db3a686 | ||
|
|
35fb9f8897 | ||
|
|
ec042434be | ||
|
|
f8041a3de5 | ||
|
|
dd90096e13 | ||
|
|
9ab5512bfa | ||
|
|
32ab56f864 | ||
|
|
78d7bfac01 | ||
|
|
57570c3ba6 | ||
|
|
ffac670f95 | ||
|
|
be16f1be44 | ||
|
|
fd0f85d565 | ||
|
|
8de5f3f0d3 | ||
|
|
0bb537b872 | ||
|
|
987da00be0 | ||
|
|
e9e2214d29 | ||
|
|
ac9f3317f1 | ||
|
|
7da85d277e | ||
|
|
274244b055 | ||
|
|
8fa611daed | ||
|
|
64d0fb17f7 | ||
|
|
6d1d844b14 | ||
|
|
686dd11d8e | ||
|
|
9d42ee9df8 | ||
|
|
9562768a04 | ||
|
|
54b8daede4 | ||
|
|
bd51afe86c | ||
|
|
a84b9bd2c8 | ||
|
|
ce1dac3b86 | ||
|
|
2dcd9f02cd | ||
|
|
416d57bec6 | ||
|
|
7d56717cf5 | ||
|
|
32ef5f47f8 | ||
|
|
32346c23e0 | ||
|
|
b6ebf61d6c | ||
|
|
d79efcedef | ||
|
|
18464ec570 | ||
|
|
ed5487a1fc | ||
|
|
df36580451 | ||
|
|
bd7790c1eb | ||
|
|
67d66c6750 | ||
|
|
1c00d7aa1a | ||
|
|
68cabe596d | ||
|
|
8919ea65e3 | ||
|
|
d4dda94e2a | ||
|
|
ee58b37d1e | ||
|
|
06cb49ec71 | ||
|
|
caca7e5860 | ||
|
|
3ddb4c9799 | ||
|
|
74c24caae9 | ||
|
|
48e8a25f6e | ||
|
|
fd045043a1 | ||
|
|
0e97696b47 | ||
|
|
804764d529 | ||
|
|
98a38754d4 | ||
|
|
837382349e | ||
|
|
d2f119b85e | ||
|
|
89d855d085 | ||
|
|
1bfafaf07c | ||
|
|
cf4f073153 | ||
|
|
aca9ba1a49 | ||
|
|
399e20a14a | ||
|
|
7ca5a0b977 | ||
|
|
74d4505b3d | ||
|
|
df74a38b90 | ||
|
|
e8d02905fe | ||
|
|
123a45149d | ||
|
|
72ec86b58d | ||
|
|
010b17509a | ||
|
|
278d593580 | ||
|
|
f997a1ff52 | ||
|
|
036d10cfbe | ||
|
|
b0c21e927b | ||
|
|
fcece3732c | ||
|
|
c7308dbbc9 | ||
|
|
9ce62dc584 | ||
|
|
0442f7012b | ||
|
|
e7f0f0ff8d | ||
|
|
4fd4b24fa2 | ||
|
|
17d18f1dd8 | ||
|
|
902e166f0c | ||
|
|
1efce51222 | ||
|
|
7da09f6296 | ||
|
|
b8d9c4c378 | ||
|
|
49b0630752 | ||
|
|
91f07b4b03 | ||
|
|
b2f4ba0882 | ||
|
|
1acd7bd19c | ||
|
|
534fc9c40c | ||
|
|
e192f10c56 | ||
|
|
676ee99709 | ||
|
|
6e5622a97a | ||
|
|
d8c9250aab | ||
|
|
986b9fb0e0 | ||
|
|
3c502c6fc2 | ||
|
|
72d59af7b0 | ||
|
|
d3eaa6600d | ||
|
|
96f41fcc02 | ||
|
|
c2b7810c33 | ||
|
|
44d4e13fa7 | ||
|
|
1dd7cd9384 | ||
|
|
8357d4675a | ||
|
|
3a4390e0c7 | ||
|
|
68fa688c96 | ||
|
|
42428261d7 | ||
|
|
e01b1ed04d | ||
|
|
4e5dcd827b | ||
|
|
e8003510ef | ||
|
|
da23e26a70 | ||
|
|
c5b781fb02 | ||
|
|
3bb1c22f49 | ||
|
|
53647fd58e | ||
|
|
0500bf070e | ||
|
|
d70b0cdd4f | ||
|
|
7e09809ad8 | ||
|
|
a7499c2de8 | ||
|
|
4c99b8c70e | ||
|
|
8b6913d31f | ||
|
|
97f0642a8b | ||
|
|
a04dd6ad31 | ||
|
|
9827c76514 | ||
|
|
712bfbae92 | ||
|
|
0152e937ec | ||
|
|
c066dc8c24 | ||
|
|
0b96b3f345 | ||
|
|
b5781933b6 | ||
|
|
d851bf8b69 | ||
|
|
b6d6a4360f | ||
|
|
69b11e8dc6 | ||
|
|
6e78037770 | ||
|
|
bed0375f45 | ||
|
|
42d723f82f | ||
|
|
fb0ce8c974 | ||
|
|
d55770f12b | ||
|
|
125c275623 | ||
|
|
c640f7ed12 | ||
|
|
73662ed7d9 | ||
|
|
a595d83232 | ||
|
|
6743b4f290 | ||
|
|
e8ecb738d0 | ||
|
|
9c20968e05 | ||
|
|
9f80690d8a | ||
|
|
e58a95e7d9 | ||
|
|
d9b96d2f31 | ||
|
|
bbe9017318 | ||
|
|
a31a68ba17 | ||
|
|
9d56e29a09 | ||
|
|
32fca6f9b3 | ||
|
|
72c2de575a | ||
|
|
93d4987dcc | ||
|
|
2a25034039 | ||
|
|
5ba1c2587d | ||
|
|
1b0a3e610e | ||
|
|
2820f3f798 | ||
|
|
09f3850250 | ||
|
|
35832f8f7f | ||
|
|
7466d8cb3a | ||
|
|
ea3786457b | ||
|
|
01322146c0 | ||
|
|
26adc557bf | ||
|
|
36e52d8165 | ||
|
|
4c0211fa23 | ||
|
|
a3b4dcf762 | ||
|
|
5537c525ca | ||
|
|
3746fd88b5 | ||
|
|
e373144350 | ||
|
|
3be4bfc821 | ||
|
|
2c1595d0d5 | ||
|
|
f08933f93c | ||
|
|
51813b7d7e | ||
|
|
4e30418f79 | ||
|
|
0af370d736 | ||
|
|
ac04a032ad | ||
|
|
f6223a6f71 | ||
|
|
937cea5a01 | ||
|
|
846522037f | ||
|
|
b5a88d00af | ||
|
|
7391271107 | ||
|
|
991694aca5 | ||
|
|
29b13d19d6 | ||
|
|
b0ff74c799 | ||
|
|
75c60660e7 | ||
|
|
eacc840c54 | ||
|
|
6d09c0adcf | ||
|
|
fe1cf1105a | ||
|
|
9454334ddd | ||
|
|
cc482c589c | ||
|
|
b387bc1038 | ||
|
|
890b735985 | ||
|
|
eb81e00d20 | ||
|
|
c619bfc05c | ||
|
|
b5935eb4c5 | ||
|
|
73b3023c2d | ||
|
|
4cf709fc26 | ||
|
|
b471b934f2 | ||
|
|
abbd4c8934 | ||
|
|
ed8c852d35 | ||
|
|
c3598b3513 | ||
|
|
b5f6a9c91a | ||
|
|
d18810b612 | ||
|
|
3c17d713b3 | ||
|
|
d46b2d52ce | ||
|
|
e07d0b3b25 | ||
|
|
2ff3b24b33 | ||
|
|
115fb05c85 | ||
|
|
1e7e72883d | ||
|
|
1edb7639f6 | ||
|
|
33c5ceecec | ||
|
|
a8a7f306fe | ||
|
|
9888e51b93 | ||
|
|
c3124314ef | ||
|
|
975cd183bc | ||
|
|
aa9da0b0c8 | ||
|
|
d83b106607 | ||
|
|
1a0a6cedc7 | ||
|
|
d4e1a83a5a | ||
|
|
03e93d546a | ||
|
|
a1b957b0f7 | ||
|
|
eeffb11841 | ||
|
|
860dc7dbc2 | ||
|
|
220f267976 | ||
|
|
b5bd6e008b | ||
|
|
9d20bc6441 | ||
|
|
1c36734092 | ||
|
|
9102b6e6b8 | ||
|
|
cd78fa9762 | ||
|
|
407e2d4b7b | ||
|
|
62d6ee46c4 | ||
|
|
f15bf8166f | ||
|
|
7ce0b225ef | ||
|
|
f853b29fd9 | ||
|
|
f9cc8de93e | ||
|
|
a924a876ae | ||
|
|
1f15d2c66e | ||
|
|
ffd8354208 | ||
|
|
b3b1f4664c | ||
|
|
03410ded9a | ||
|
|
09c9671376 | ||
|
|
736b3b93e9 | ||
|
|
4a71ca6b81 | ||
|
|
a828f9b1f9 | ||
|
|
7ce5f8e8b6 | ||
|
|
08b7e036b4 | ||
|
|
b1722a085e | ||
|
|
9d985585b9 | ||
|
|
de057b7234 | ||
|
|
04ec49e18e | ||
|
|
34c7c25908 | ||
|
|
f74374e759 | ||
|
|
cce74f5631 | ||
|
|
003b9e48e1 | ||
|
|
28bf73cd5a | ||
|
|
511b982dce | ||
|
|
dcd176f95c | ||
|
|
d394aa8a15 | ||
|
|
c8b35d5ce1 | ||
|
|
6cd107c3e3 | ||
|
|
a884e32816 | ||
|
|
45d2d3cbb5 | ||
|
|
382f75d249 | ||
|
|
e90010f0c0 | ||
|
|
d6aaed1d82 | ||
|
|
5082ed3b9e | ||
|
|
a858e07db0 | ||
|
|
df8f785731 | ||
|
|
332674a4a1 | ||
|
|
a686849bb6 | ||
|
|
54d041701c | ||
|
|
496eec17a1 | ||
|
|
25eb67c582 | ||
|
|
a26fab3cce | ||
|
|
7cb67cf8fb | ||
|
|
96ec1e937f | ||
|
|
b6616ed2cc | ||
|
|
7e263af75f | ||
|
|
b4c51f3d41 | ||
|
|
1dfbaa1e02 | ||
|
|
bd717349a7 | ||
|
|
99d7752e25 | ||
|
|
22e58d7623 | ||
|
|
30ad142868 | ||
|
|
e084ff4f7b | ||
|
|
3bbec4081a | ||
|
|
45137d5506 | ||
|
|
e9d9a656ab | ||
|
|
ade458b820 | ||
|
|
b022dcbb70 | ||
|
|
b403a7a25d | ||
|
|
a6632632fa | ||
|
|
b863ea51ad | ||
|
|
796b66b057 | ||
|
|
2626dcbc5f | ||
|
|
105a758914 | ||
|
|
98cce186c7 | ||
|
|
474e13f8b9 | ||
|
|
09f87238dc | ||
|
|
2b3f87d6f2 | ||
|
|
9d3c823603 | ||
|
|
7e00d70f4d | ||
|
|
0828f747e3 | ||
|
|
d7de0c2578 | ||
|
|
4dfc82f684 | ||
|
|
92748de7d4 | ||
|
|
0e7e27f99d | ||
|
|
23147f2328 | ||
|
|
3811f41076 | ||
|
|
43a0a4f8e0 | ||
|
|
66d1af63b0 | ||
|
|
bc68f400f6 | ||
|
|
67ee3a5a67 | ||
|
|
b407893db5 | ||
|
|
1b4a41b522 | ||
|
|
ac7f8a6447 | ||
|
|
da57fcb641 | ||
|
|
f11c332cb4 | ||
|
|
3b4006b821 | ||
|
|
4c62d8c1b2 | ||
|
|
4f47d4482b | ||
|
|
ad062486ff | ||
|
|
33c8bdfabf | ||
|
|
0365c94407 | ||
|
|
c81c4f9114 | ||
|
|
d920953df1 | ||
|
|
f02a2e44d8 | ||
|
|
fb1aa9c028 | ||
|
|
d4a1d4cd7e | ||
|
|
8a6d9a1496 | ||
|
|
a8eefbc9f0 | ||
|
|
75521fe363 | ||
|
|
c75778943f | ||
|
|
0e6fa37ae4 | ||
|
|
819eea9456 | ||
|
|
25eb8dc9b0 | ||
|
|
40067f5aa2 | ||
|
|
1ffc10e44f | ||
|
|
5d5547ffef | ||
|
|
e83c28bf54 | ||
|
|
a3c0911529 | ||
|
|
ae23f0de03 | ||
|
|
edb5529df3 | ||
|
|
4d3fb77786 | ||
|
|
24ac7c1626 | ||
|
|
9b2ec62be9 | ||
|
|
4252b5e273 | ||
|
|
4e8cbe3db1 | ||
|
|
bdf8bbe26f | ||
|
|
990c05fc3d | ||
|
|
7bb4e22a77 | ||
|
|
736503df1b | ||
|
|
414455a8fb | ||
|
|
bc0ab88e74 | ||
|
|
01ec539065 | ||
|
|
b9792fc17d | ||
|
|
c656c3c087 | ||
|
|
a6c1d2d486 | ||
|
|
44bce59777 | ||
|
|
9b5e5aa474 | ||
|
|
a5d02998ad | ||
|
|
1d4c129e9c | ||
|
|
e9a6ca8ebc | ||
|
|
28340c80dd | ||
|
|
05f6fde467 | ||
|
|
56d353cb64 | ||
|
|
260d0cdc67 | ||
|
|
cdd92303b8 | ||
|
|
0dba37f4f7 | ||
|
|
7f3775a061 | ||
|
|
a7a10f4eaa | ||
|
|
5166083406 | ||
|
|
5c4d95ac0f | ||
|
|
71c4d74759 | ||
|
|
725e8221a5 | ||
|
|
92ae41cc13 | ||
|
|
333b9130fe | ||
|
|
60ae903cf3 | ||
|
|
24ea55b010 | ||
|
|
8231d07706 | ||
|
|
98df2b111e | ||
|
|
d8875f381b | ||
|
|
6e3d16173a | ||
|
|
a88b189664 | ||
|
|
48da00eb66 | ||
|
|
5322332c5d | ||
|
|
c92cfbb20a | ||
|
|
f673647072 | ||
|
|
7ea5a9bba3 | ||
|
|
aa002c5d60 | ||
|
|
64020758d9 | ||
|
|
a4f357fd80 | ||
|
|
4c6fdfd76a | ||
|
|
c972452310 | ||
|
|
6ba6a16836 | ||
|
|
c63f1dfc53 | ||
|
|
ac79c45529 | ||
|
|
7f58737f1f | ||
|
|
ead8a48436 | ||
|
|
178d33155f | ||
|
|
07ab8e508c | ||
|
|
ea1d4adfa6 | ||
|
|
f34a8ef0e5 | ||
|
|
0df4b39bcc | ||
|
|
07eca00bd5 | ||
|
|
cf8386aa50 | ||
|
|
44de6a5549 | ||
|
|
a41a9bcbf7 | ||
|
|
a679fae9c0 | ||
|
|
0f906f3937 | ||
|
|
c907daa741 | ||
|
|
df579de147 | ||
|
|
e884bdbbc4 | ||
|
|
84fc70e9a9 | ||
|
|
4fc5d3f03b | ||
|
|
9402516acd |
@@ -1,8 +1,16 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||
[target.i686-pc-windows-msvc]
|
||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
|
||||
[target.'cfg(target_os="macos")']
|
||||
rustflags = [
|
||||
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
|
||||
]
|
||||
#[target.'cfg(target_os="linux")']
|
||||
# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
|
||||
# this is unlikely to help also, because the other so files still use libc dynamically
|
||||
#rustflags = [
|
||||
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
|
||||
#]
|
||||
[net]
|
||||
git-fetch-with-cli = true
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -26,8 +26,8 @@ body:
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system(s) on local side and remote side
|
||||
description: What operating system(s) do you see this bug on? local side -> remote side.
|
||||
label: Operating system(s) on local (controlling) side and remote (controlled) side
|
||||
description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
|
||||
placeholder: |
|
||||
Windows 10 -> osx
|
||||
validations:
|
||||
@@ -35,8 +35,8 @@ body:
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: RustDesk Version(s) on local side and remote side
|
||||
description: What RustDesk version(s) do you see this bug on? local side -> remote side.
|
||||
label: RustDesk Version(s) on local (controlling) side and remote (controlled) side
|
||||
description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
|
||||
placeholder: |
|
||||
1.1.9 -> 1.1.8
|
||||
validations:
|
||||
|
||||
42
.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
vendored
Normal file
42
.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart
|
||||
index 7e634cd2aa..c1e9acc295 100644
|
||||
--- a/packages/flutter/lib/src/material/dropdown_menu.dart
|
||||
+++ b/packages/flutter/lib/src/material/dropdown_menu.dart
|
||||
@@ -475,7 +475,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
final GlobalKey _leadingKey = GlobalKey();
|
||||
late List<GlobalKey> buttonItemKeys;
|
||||
final MenuController _controller = MenuController();
|
||||
- late bool _enableFilter;
|
||||
+ bool _enableFilter = false;
|
||||
late List<DropdownMenuEntry<T>> filteredEntries;
|
||||
List<Widget>? _initialMenu;
|
||||
int? currentHighlight;
|
||||
@@ -524,6 +524,11 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
}
|
||||
_localTextEditingController = widget.controller ?? TextEditingController();
|
||||
}
|
||||
+ if (oldWidget.enableFilter != widget.enableFilter) {
|
||||
+ if (!widget.enableFilter) {
|
||||
+ _enableFilter = false;
|
||||
+ }
|
||||
+ }
|
||||
if (oldWidget.enableSearch != widget.enableSearch) {
|
||||
if (!widget.enableSearch) {
|
||||
currentHighlight = null;
|
||||
@@ -663,6 +668,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
);
|
||||
currentHighlight = widget.enableSearch ? i : null;
|
||||
widget.onSelected?.call(entry.value);
|
||||
+ _enableFilter = false;
|
||||
}
|
||||
: null,
|
||||
requestFocusOnHover: false,
|
||||
@@ -735,6 +741,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
|
||||
if (_enableFilter) {
|
||||
filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text)
|
||||
?? filter(widget.dropdownMenuEntries, _localTextEditingController!);
|
||||
+ } else {
|
||||
+ filteredEntries = widget.dropdownMenuEntries;
|
||||
}
|
||||
|
||||
if (widget.enableSearch) {
|
||||
18
.github/workflows/bridge.yml
vendored
18
.github/workflows/bridge.yml
vendored
@@ -6,8 +6,9 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
|
||||
jobs:
|
||||
generate_bridge:
|
||||
@@ -37,10 +38,8 @@ jobs:
|
||||
git \
|
||||
g++ \
|
||||
libclang-10-dev \
|
||||
libclang-dev \
|
||||
libgtk-3-dev \
|
||||
llvm-10-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
@@ -49,9 +48,9 @@ jobs:
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: ''
|
||||
components: "rustfmt"
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -74,12 +73,13 @@ jobs:
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||
pushd flutter && flutter pub get && popd
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
|
||||
pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
|
||||
|
||||
- name: Run flutter rust bridge
|
||||
run: |
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||
cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@master
|
||||
@@ -90,3 +90,5 @@ jobs:
|
||||
./src/bridge_generated.io.rs
|
||||
./flutter/lib/generated_bridge.dart
|
||||
./flutter/lib/generated_bridge.freezed.dart
|
||||
./flutter/macos/Runner/bridge_generated.h
|
||||
./flutter/ios/Runner/bridge_generated.h
|
||||
|
||||
61
.github/workflows/build-macos-arm64.yml
vendored
61
.github/workflows/build-macos-arm64.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Flutter Nightly MacOS Arm64 Build
|
||||
|
||||
on:
|
||||
#schedule:
|
||||
# schedule build every night
|
||||
# - cron: "0/6 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_NDK_VERSION: "3.1.2"
|
||||
LLVM_VERSION: "15.0.6"
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
# for arm64 linux because official Dart SDK does not work
|
||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||
FLUTTER_ELINUX_COMMIT_ID: "c02bd16e1630f5bd690b85c5c2456ac1920e25af"
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2023.10.19
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
||||
VERSION: "1.2.4"
|
||||
NDK_VERSION: "r26b"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
|
||||
MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
|
||||
# To make a custom build with your own servers set the below secret values
|
||||
RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}'
|
||||
RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}'
|
||||
API_SERVER: '${{ secrets.API_SERVER }}'
|
||||
UPLOAD_ARTIFACT: true
|
||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||
|
||||
jobs:
|
||||
build-for-macOS-arm64:
|
||||
name: build-for-macOS-arm64
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
steps:
|
||||
#- name: Import the codesign cert
|
||||
# if: env.MACOS_P12_BASE64 != null
|
||||
# uses: apple-actions/import-codesign-certs@v1
|
||||
# continue-on-error: true
|
||||
# with:
|
||||
# p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
||||
# p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
||||
# keychain: rustdesk
|
||||
|
||||
#- name: Check sign and import sign key
|
||||
# if: env.MACOS_P12_BASE64 != null
|
||||
# run: |
|
||||
# security default-keychain -s rustdesk.keychain
|
||||
# security find-identity -v
|
||||
|
||||
- name: Run
|
||||
shell: bash
|
||||
run: |
|
||||
cd /opt/build
|
||||
#./update_mac_template.sh
|
||||
#security default-keychain -s rustdesk.keychain
|
||||
#security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
|
||||
./agent.sh
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -4,9 +4,9 @@ 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
|
||||
# vcpkg version: 2024.06.15
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -21,6 +21,9 @@ on:
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- "res/**"
|
||||
- "appimage/**"
|
||||
- "flatpak/**"
|
||||
|
||||
jobs:
|
||||
# ensure_cargo_fmt:
|
||||
@@ -71,12 +74,12 @@ jobs:
|
||||
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
@@ -103,11 +106,14 @@ jobs:
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libpam0g-dev \
|
||||
libasound2-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
|
||||
39
.github/workflows/fdroid.yml
vendored
Normal file
39
.github/workflows/fdroid.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Fdroid version file generation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||
- '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||
|
||||
jobs:
|
||||
# https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml
|
||||
# Finds latest release and transforms F-Droid version code from version as follows:
|
||||
# X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A
|
||||
update-fdroid-version-file:
|
||||
name: Publish RustDesk version file for F-Droid updater
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate RustDesk version file
|
||||
run: |
|
||||
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||
UPSTREAM_VERNAME="${GITHUB_REF##refs/tags/}"
|
||||
UPSTREAM_VERNAME="${UPSTREAM_VERNAME##v}"
|
||||
else
|
||||
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
|
||||
fi
|
||||
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
|
||||
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
|
||||
2153
.github/workflows/flutter-build.yml
vendored
2153
.github/workflows/flutter-build.yml
vendored
File diff suppressed because it is too large
Load Diff
3
.github/workflows/flutter-ci.yml
vendored
3
.github/workflows/flutter-ci.yml
vendored
@@ -13,6 +13,9 @@ on:
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- "res/**"
|
||||
- "appimage/**"
|
||||
- "flatpak/**"
|
||||
|
||||
jobs:
|
||||
run-ci:
|
||||
|
||||
22
.github/workflows/flutter-tag.yml
vendored
22
.github/workflows/flutter-tag.yml
vendored
@@ -15,24 +15,4 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
upload-artifact: true
|
||||
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
|
||||
upload-tag: ${{ github.ref_name }}
|
||||
84
.github/workflows/history.yml
vendored
84
.github/workflows/history.yml
vendored
@@ -1,84 +0,0 @@
|
||||
name: Flutter Windows History Build
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
env:
|
||||
LLVM_VERSION: "10.0"
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
TAG_NAME: "tmp"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VERSION: "1.2.4"
|
||||
|
||||
jobs:
|
||||
build-for-history-windows:
|
||||
name: ${{ matrix.job.date }}
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { 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@v4
|
||||
with:
|
||||
ref: ${{ matrix.job.ref }}
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v1
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||
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: |
|
||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
run: python3 .\build.py --portable --hwcodec --flutter
|
||||
|
||||
- name: Build self-extracted executable
|
||||
shell: bash
|
||||
run: |
|
||||
pushd ./libs/portable
|
||||
python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
|
||||
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:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
./SignOutput/rustdesk-*.exe
|
||||
416
.github/workflows/playground.yml
vendored
Normal file
416
.github/workflows/playground.yml
vendored
Normal file
@@ -0,0 +1,416 @@
|
||||
name: playground
|
||||
|
||||
on:
|
||||
#schedule:
|
||||
# schedule build every night
|
||||
# - cron: "0/6 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
CARGO_NDK_VERSION: "3.1.2"
|
||||
LLVM_VERSION: "15.0.6"
|
||||
FLUTTER_VERSION: "3.22.2"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
# for arm64 linux because official Dart SDK does not work
|
||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.06.15
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
VERSION: "1.3.5"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||
# To make a custom build with your own servers set the below secret values
|
||||
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
||||
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
||||
API_SERVER: "${{ secrets.API_SERVER }}"
|
||||
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||
|
||||
jobs:
|
||||
build-for-macOS:
|
||||
name: ${{ matrix.job.target }}
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||
extra-build-args: "",
|
||||
arch: x86_64,
|
||||
flutter: "3.13.9",
|
||||
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
|
||||
bridge: "1.80.1",
|
||||
date: "20231219"
|
||||
}
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||
extra-build-args: "",
|
||||
arch: x86_64,
|
||||
flutter: "3.10.6",
|
||||
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
|
||||
bridge: "1.80.1",
|
||||
date: "20231219"
|
||||
}
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||
extra-build-args: "",
|
||||
arch: x86_64,
|
||||
flutter: "3.10.6",
|
||||
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
|
||||
bridge: "1.80.1",
|
||||
date: "20231119"
|
||||
}
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||
extra-build-args: "",
|
||||
arch: x86_64,
|
||||
flutter: "3.13.9",
|
||||
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
|
||||
bridge: "1.80.1",
|
||||
date: "20231119"
|
||||
}
|
||||
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
|
||||
with:
|
||||
ref: ${{ matrix.job.ref }}
|
||||
|
||||
- name: Import the codesign cert
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
||||
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
||||
keychain: rustdesk
|
||||
|
||||
- name: Check sign and import sign key
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
run: |
|
||||
security default-keychain -s rustdesk.keychain
|
||||
security find-identity -v
|
||||
|
||||
- name: Import notarize key
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
|
||||
fileName: rustdesk.json
|
||||
fileDir: ${{ github.workspace }}
|
||||
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
|
||||
|
||||
- name: Install rcodesign tool
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
shell: bash
|
||||
run: |
|
||||
pushd /tmp
|
||||
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
|
||||
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
|
||||
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
|
||||
popd
|
||||
|
||||
- name: Install build runtime
|
||||
run: |
|
||||
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ matrix.job.flutter }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: "rustfmt"
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" --locked
|
||||
# below works for mac to make buildable on 3.13.9
|
||||
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
|
||||
pushd flutter && flutter pub get && popd
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||
|
||||
- name: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
if: false
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
if: false
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
|
||||
|
||||
- name: Show version information (Rust, cargo, Clang)
|
||||
shell: bash
|
||||
run: |
|
||||
clang --version || true
|
||||
rustup -V
|
||||
rustup toolchain list
|
||||
rustup default
|
||||
cargo -V
|
||||
rustc -V
|
||||
|
||||
- name: Build rustdesk
|
||||
run: |
|
||||
./build.py --flutter ${{ matrix.job.extra-build-args }}
|
||||
|
||||
- name: create unsigned dmg
|
||||
run: |
|
||||
CREATE_DMG="$(command -v create-dmg)"
|
||||
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||
|
||||
- name: Codesign app and create signed dmg
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
run: |
|
||||
# Patch create-dmg to give more attempts to unmount image
|
||||
CREATE_DMG="$(command -v create-dmg)"
|
||||
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||
# Unlock keychain
|
||||
security default-keychain -s rustdesk.keychain
|
||||
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
|
||||
# start sign the rustdesk.app and dmg
|
||||
rm -rf *.dmg || true
|
||||
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
|
||||
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
|
||||
# notarize the rustdesk-${{ env.VERSION }}.dmg
|
||||
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
|
||||
|
||||
- name: Rename rustdesk
|
||||
run: |
|
||||
for name in rustdesk*??.dmg; do
|
||||
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg"
|
||||
done
|
||||
|
||||
- name: Publish DMG package
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
rustdesk*-${{ matrix.job.arch }}*.dmg
|
||||
|
||||
|
||||
build-rustdesk-android:
|
||||
if: false
|
||||
name: build rustdesk android apk ${{ matrix.job.target }}
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-20.04,
|
||||
openssl-arch: android-arm64,
|
||||
ref: master, # latest
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ matrix.job.ref }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
clang \
|
||||
cmake \
|
||||
curl \
|
||||
gcc-multilib \
|
||||
git \
|
||||
g++ \
|
||||
g++-multilib \
|
||||
libayatana-appindicator3-dev\
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-10-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-10-dev \
|
||||
nasm \
|
||||
yasm \
|
||||
ninja-build \
|
||||
openjdk-11-jdk-headless \
|
||||
pkg-config \
|
||||
tree \
|
||||
wget
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
components: "rustfmt"
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
|
||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
||||
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
|
||||
pushd flutter ; flutter pub get ; popd
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: ${{ env.NDK_VERSION }}
|
||||
add-to-path: true
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
case ${{ matrix.job.target }} in
|
||||
aarch64-linux-android)
|
||||
./flutter/build_android_deps.sh arm64-v8a
|
||||
;;
|
||||
armv7-linux-androideabi)
|
||||
./flutter/build_android_deps.sh armeabi-v7a
|
||||
;;
|
||||
esac
|
||||
shell: bash
|
||||
|
||||
- name: Clone deps
|
||||
shell: bash
|
||||
run: |
|
||||
pushd /opt
|
||||
git clone https://github.com/rustdesk-org/rustdesk_thirdparty_lib.git --depth=1
|
||||
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
|
||||
# cp -rf /opt/rustdesk_thirdparty_lib/vcpkg/* /opt/artifacts/vcpkg/
|
||||
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
|
||||
|
||||
- name: Build rustdesk lib
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
rustup target add ${{ matrix.job.target }}
|
||||
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
|
||||
case ${{ matrix.job.target }} in
|
||||
aarch64-linux-android)
|
||||
./flutter/ndk_arm64.sh
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||
;;
|
||||
armv7-linux-androideabi)
|
||||
./flutter/ndk_arm.sh
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Build rustdesk
|
||||
shell: bash
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
case ${{ matrix.job.target }} in
|
||||
aarch64-linux-android)
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||
# build flutter
|
||||
pushd flutter
|
||||
flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||
;;
|
||||
armv7-linux-androideabi)
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||
# build flutter
|
||||
pushd flutter
|
||||
flutter build apk --release --target-platform android-arm --split-per-abi
|
||||
mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||
;;
|
||||
esac
|
||||
popd
|
||||
mkdir -p signed-apk; pushd signed-apk
|
||||
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
|
||||
|
||||
- uses: r0adkll/sign-android-release@v1
|
||||
name: Sign app APK
|
||||
if: env.ANDROID_SIGNING_KEY != null
|
||||
id: sign-rustdesk
|
||||
with:
|
||||
releaseDirectory: ./signed-apk
|
||||
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
|
||||
alias: ${{ secrets.ANDROID_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
env:
|
||||
# override default build-tools version (29.0.3) -- optional
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
|
||||
- name: Publish signed apk package
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
description: 'Target'
|
||||
required: true
|
||||
type: string
|
||||
default: 'windows-2019'
|
||||
default: 'windows-2022'
|
||||
configuration:
|
||||
description: 'Configuration'
|
||||
required: true
|
||||
|
||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -4,9 +4,9 @@ on:
|
||||
types: [released]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest # action can only be run on windows
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v1
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -51,4 +51,7 @@ lib/generated_bridge.dart
|
||||
# build cache in examples
|
||||
examples/**/target/
|
||||
# ===
|
||||
vcpkg_installed
|
||||
vcpkg_installed
|
||||
flutter/lib/generated_plugin_registrant.dart
|
||||
libsciter.dylib
|
||||
flutter/web/
|
||||
3068
Cargo.lock
generated
3068
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
57
Cargo.toml
57
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.2.4"
|
||||
version = "1.3.5"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -19,19 +19,14 @@ path = "src/naming.rs"
|
||||
[features]
|
||||
inline = []
|
||||
cli = []
|
||||
flutter_texture_render = []
|
||||
appimage = []
|
||||
flatpak = []
|
||||
use_samplerate = ["samplerate"]
|
||||
use_rubato = ["rubato"]
|
||||
use_dasp = ["dasp"]
|
||||
flutter = ["flutter_rust_bridge"]
|
||||
default = ["use_dasp"]
|
||||
hwcodec = ["scrap/hwcodec"]
|
||||
gpucodec = ["scrap/gpucodec"]
|
||||
vram = ["scrap/vram"]
|
||||
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 = [
|
||||
@@ -41,6 +36,7 @@ unix-file-copy-paste = [
|
||||
"dep:once_cell",
|
||||
"clipboard/unix-file-copy-paste",
|
||||
]
|
||||
screencapturekit = ["cpal/screencapturekit"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -65,14 +61,13 @@ samplerate = { version = "0.2", optional = true }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
clap = "4.2"
|
||||
rpassword = "7.2"
|
||||
base64 = "0.21"
|
||||
num_cpus = "1.15"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
default-net = "0.14"
|
||||
wol-rs = "1.0"
|
||||
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
||||
errno = "0.3"
|
||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
||||
rdev = { git = "https://github.com/rustdesk-org/rdev" }
|
||||
url = { version = "2.3", features = ["serde"] }
|
||||
crossbeam-queue = "0.3"
|
||||
hex = "0.4"
|
||||
@@ -83,9 +78,7 @@ 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"
|
||||
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
|
||||
ringbuf = "0.3"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
@@ -95,16 +88,32 @@ sys-locale = "0.3"
|
||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||
clipboard = { path = "libs/clipboard" }
|
||||
ctrlc = "3.2"
|
||||
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
|
||||
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
|
||||
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
|
||||
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||
|
||||
system_shutdown = "4.0"
|
||||
qrcode-generator = "4.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] }
|
||||
winapi = { version = "0.3", features = [
|
||||
"winuser",
|
||||
"wincrypt",
|
||||
"shellscalingapi",
|
||||
"pdh",
|
||||
"synchapi",
|
||||
"memoryapi",
|
||||
"shellapi",
|
||||
"devguid",
|
||||
"setupapi",
|
||||
"cguid",
|
||||
"cfgmgr32",
|
||||
"ioapiset",
|
||||
] }
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
virtual_display = { path = "libs/virtual_display", optional = true }
|
||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
||||
virtual_display = { path = "libs/virtual_display" }
|
||||
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
|
||||
shared_memory = "0.12"
|
||||
tauri-winrt-notification = "0.1.2"
|
||||
runas = "1.2"
|
||||
@@ -128,35 +137,37 @@ image = "0.24"
|
||||
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" }
|
||||
wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false }
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "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" }
|
||||
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
||||
async-process = "1.7"
|
||||
mouce = { git="https://github.com/fufesou/mouce.git" }
|
||||
evdev = { git="https://github.com/fufesou/evdev" }
|
||||
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
pam = { git="https://github.com/fufesou/pam", optional = true }
|
||||
pam = { git="https://github.com/rustdesk-org/pam" }
|
||||
users = { version = "0.11" }
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
percent-encoding = {version = "2.3", optional = true}
|
||||
once_cell = {version = "1.18", optional = true}
|
||||
nix = { version = "0.29", features = ["term", "process"]}
|
||||
gtk = "0.18"
|
||||
termios = "0.3"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
jni = "0.21"
|
||||
android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
|
||||
android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
|
||||
|
||||
[workspace]
|
||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
||||
|
||||
@@ -18,6 +18,7 @@ RUN apt update -y && \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
libasound2-dev \
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
make \
|
||||
cmake \
|
||||
|
||||
12
README.md
12
README.md
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
@@ -59,19 +59,19 @@ Please download Sciter dynamic library yourself.
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
@@ -171,3 +171,7 @@ Please ensure that you are running these commands from the root of the RustDesk
|
||||

|
||||
|
||||

|
||||
|
||||
## [Public Servers](#public-servers)
|
||||
|
||||
RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
version: 1
|
||||
script:
|
||||
- rm -rf ./AppDir || true
|
||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
||||
- bsdtar -zxvf rustdesk.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.4
|
||||
version: 1.3.5
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -26,18 +26,18 @@ AppDir:
|
||||
- arm64
|
||||
allow_unauthenticated: true
|
||||
sources:
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted
|
||||
universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted
|
||||
universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
include:
|
||||
- libc6
|
||||
- libc6:arm64
|
||||
- libgtk-3-0
|
||||
- libxcb-randr0
|
||||
- libxdo3
|
||||
@@ -47,13 +47,19 @@ AppDir:
|
||||
- libasound2
|
||||
- libsystemd0
|
||||
- curl
|
||||
- libva2
|
||||
- libva-drm2
|
||||
- libva-x11-2
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- gstreamer1.0-pipewire
|
||||
- libwayland-client0
|
||||
- libwayland-cursor0
|
||||
- libwayland-egl1
|
||||
- libpulse0
|
||||
- packagekit-gtk3-module
|
||||
- libcanberra-gtk3-module
|
||||
- libpam0g
|
||||
- libdrm2
|
||||
exclude:
|
||||
- humanity-icon-theme
|
||||
- hicolor-icon-theme
|
||||
@@ -69,8 +75,11 @@ AppDir:
|
||||
- usr/share/doc/*/TODO.*
|
||||
runtime:
|
||||
env:
|
||||
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
|
||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
|
||||
GDK_BACKEND: x11
|
||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
|
||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||
test:
|
||||
fedora-30:
|
||||
image: appimagecrafters/tests-env:fedora-30
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
version: 1
|
||||
script:
|
||||
- rm -rf ./AppDir || true
|
||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
||||
- bsdtar -zxvf rustdesk.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.4
|
||||
version: 1.3.5
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -26,19 +26,20 @@ AppDir:
|
||||
- amd64
|
||||
allow_unauthenticated: true
|
||||
sources:
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted
|
||||
universe multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
||||
universe multiverse
|
||||
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
|
||||
bionic main
|
||||
include:
|
||||
# https://github.com/rustdesk/rustdesk/issues/9103
|
||||
# Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try.
|
||||
# We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it.
|
||||
- libc6:amd64
|
||||
- libgtk-3-0
|
||||
- libxcb-randr0
|
||||
@@ -49,14 +50,19 @@ AppDir:
|
||||
- libasound2
|
||||
- libsystemd0
|
||||
- curl
|
||||
- libva2
|
||||
- libva-drm2
|
||||
- libva-x11-2
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- gstreamer1.0-pipewire
|
||||
- libwayland-client0
|
||||
- libwayland-cursor0
|
||||
- libwayland-egl1
|
||||
- libpulse0
|
||||
- packagekit-gtk3-module
|
||||
- libcanberra-gtk3-module
|
||||
- libpam0g
|
||||
- libdrm2
|
||||
exclude:
|
||||
- humanity-icon-theme
|
||||
- hicolor-icon-theme
|
||||
@@ -72,8 +78,11 @@ AppDir:
|
||||
- usr/share/doc/*/TODO.*
|
||||
runtime:
|
||||
env:
|
||||
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
|
||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
|
||||
GDK_BACKEND: x11
|
||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
|
||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||
test:
|
||||
fedora-30:
|
||||
image: appimagecrafters/tests-env:fedora-30
|
||||
|
||||
73
build.py
73
build.py
@@ -25,17 +25,22 @@ flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
|
||||
skip_cargo = False
|
||||
|
||||
|
||||
def get_arch() -> str:
|
||||
custom_arch = os.environ.get("ARCH")
|
||||
def get_deb_arch() -> str:
|
||||
custom_arch = os.environ.get("DEB_ARCH")
|
||||
if custom_arch is None:
|
||||
return "amd64"
|
||||
return custom_arch
|
||||
|
||||
def get_deb_extra_depends() -> str:
|
||||
custom_arch = os.environ.get("DEB_ARCH")
|
||||
if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so
|
||||
return ", libatomic1"
|
||||
return ""
|
||||
|
||||
def system2(cmd):
|
||||
err = os.system(cmd)
|
||||
if err != 0:
|
||||
print(f"Error occurred when executing: {cmd}. Exiting.")
|
||||
exit_code = os.system(cmd)
|
||||
if exit_code != 0:
|
||||
sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n")
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
@@ -48,15 +53,7 @@ def get_version():
|
||||
|
||||
|
||||
def parse_rc_features(feature):
|
||||
available_features = {
|
||||
'PrivacyMode': {
|
||||
'platform': ['windows'],
|
||||
'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'],
|
||||
}
|
||||
}
|
||||
available_features = {}
|
||||
apply_features = {}
|
||||
if not feature:
|
||||
feature = []
|
||||
@@ -81,7 +78,6 @@ def parse_rc_features(feature):
|
||||
elif isinstance(feature, list):
|
||||
if windows:
|
||||
# download third party is deprecated, we use github ci instead.
|
||||
# force add PrivacyMode
|
||||
# feature.append('PrivacyMode')
|
||||
pass
|
||||
for feat in feature:
|
||||
@@ -108,19 +104,19 @@ def make_parser():
|
||||
nargs='+',
|
||||
default='',
|
||||
help='Integrate features, windows only.'
|
||||
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
||||
'Available: [Not used for now]. 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(
|
||||
'--hwcodec',
|
||||
action='store_true',
|
||||
help='Enable feature hwcodec' + (
|
||||
'' if windows or osx else ', need libva-dev, libvdpau-dev.')
|
||||
'' if windows or osx else ', need libva-dev.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--gpucodec',
|
||||
'--vram',
|
||||
action='store_true',
|
||||
help='Enable feature gpucodec, only available on windows now.'
|
||||
help='Enable feature vram, only available on windows now.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--portable',
|
||||
@@ -132,16 +128,6 @@ def make_parser():
|
||||
action='store_true',
|
||||
help='Build with unix file copy paste feature'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--flatpak',
|
||||
action='store_true',
|
||||
help='Build rustdesk libs with the flatpak feature enabled'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--appimage',
|
||||
action='store_true',
|
||||
help='Build rustdesk libs with the appimage feature enabled'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-cargo',
|
||||
action='store_true',
|
||||
@@ -157,6 +143,12 @@ def make_parser():
|
||||
"--package",
|
||||
type=str
|
||||
)
|
||||
if osx:
|
||||
parser.add_argument(
|
||||
'--screencapturekit',
|
||||
action='store_true',
|
||||
help='Enable feature screencapturekit'
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -282,17 +274,15 @@ def get_features(args):
|
||||
features = ['inline'] if not args.flutter else []
|
||||
if args.hwcodec:
|
||||
features.append('hwcodec')
|
||||
if args.gpucodec:
|
||||
features.append('gpucodec')
|
||||
if args.vram:
|
||||
features.append('vram')
|
||||
if args.flutter:
|
||||
features.append('flutter')
|
||||
features.append('flutter_texture_render')
|
||||
if args.flatpak:
|
||||
features.append('flatpak')
|
||||
if args.appimage:
|
||||
features.append('appimage')
|
||||
if args.unix_file_copy_paste:
|
||||
features.append('unix-file-copy-paste')
|
||||
if osx:
|
||||
if args.screencapturekit:
|
||||
features.append('screencapturekit')
|
||||
print("features:", features)
|
||||
return features
|
||||
|
||||
@@ -302,14 +292,17 @@ def generate_control_file(version):
|
||||
system2('/bin/rm -rf %s' % control_file_path)
|
||||
|
||||
content = """Package: rustdesk
|
||||
Section: net
|
||||
Priority: optional
|
||||
Version: %s
|
||||
Architecture: %s
|
||||
Maintainer: rustdesk <info@rustdesk.com>
|
||||
Homepage: https://rustdesk.com
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
||||
Recommends: libayatana-appindicator3-1
|
||||
Description: A remote control software.
|
||||
|
||||
""" % (version, get_arch())
|
||||
""" % (version, get_deb_arch(), get_deb_extra_depends())
|
||||
file = open(control_file_path, "w")
|
||||
file.write(content)
|
||||
file.close()
|
||||
@@ -349,8 +342,6 @@ def build_flutter_deb(version, features):
|
||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||
system2(
|
||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||
system2(
|
||||
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
|
||||
system2(
|
||||
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||
system2(
|
||||
@@ -394,8 +385,6 @@ def build_deb_from_folder(version, binary_folder):
|
||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||
system2(
|
||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||
system2(
|
||||
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
|
||||
system2(
|
||||
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
||||
|
||||
|
||||
9
build.rs
9
build.rs
@@ -1,7 +1,7 @@
|
||||
#[cfg(windows)]
|
||||
fn build_windows() {
|
||||
let file = "src/platform/windows.cc";
|
||||
let file2 = "src/platform/windows_delete_test_cert.cc";
|
||||
let file2 = "src/platform/windows_delete_test_cert.cc";
|
||||
cc::Build::new().file(file).file(file2).compile("windows");
|
||||
println!("cargo:rustc-link-lib=WtsApi32");
|
||||
println!("cargo:rerun-if-changed={}", file);
|
||||
@@ -61,7 +61,11 @@ fn install_android_deps() {
|
||||
let target = format!("{}-android", target_arch);
|
||||
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
|
||||
let mut path: std::path::PathBuf = vcpkg_root.into();
|
||||
path.push("installed");
|
||||
if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") {
|
||||
path = vcpkg_root.into();
|
||||
} else {
|
||||
path.push("installed");
|
||||
}
|
||||
path.push(target);
|
||||
println!(
|
||||
"{}",
|
||||
@@ -72,7 +76,6 @@ fn install_android_deps() {
|
||||
);
|
||||
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");
|
||||
}
|
||||
|
||||
87
docs/CODE_OF_CONDUCT-ZH.md
Normal file
87
docs/CODE_OF_CONDUCT-ZH.md
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
# 贡献者公约行为准则
|
||||
|
||||
## 我们的承诺
|
||||
|
||||
身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。
|
||||
|
||||
我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。
|
||||
|
||||
## 我们的标准
|
||||
|
||||
有助于为我们的社区创造积极环境的行为例子包括但不限于:
|
||||
|
||||
* 表现出对他人的同情和善意
|
||||
* 尊重不同的主张、观点和感受
|
||||
* 提出和大方接受建设性意见
|
||||
* 承担责任并向受我们错误影响的人道歉
|
||||
* 注重社区共同诉求,而非个人得失
|
||||
|
||||
不当行为例子包括:
|
||||
|
||||
* 使用情色化的语言或图像,及性引诱或挑逗
|
||||
* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击
|
||||
* 公开或私下的骚扰行为
|
||||
* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址
|
||||
* 其他有理由认定为违反职业操守的不当行为
|
||||
|
||||
## 责任和权力
|
||||
|
||||
社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。
|
||||
|
||||
社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论(comment)、提交(commits)、代码、维基(wiki)编辑、议题(issues)或其他贡献,并在适当时机知采取措施的理由。
|
||||
|
||||
## 适用范围
|
||||
|
||||
本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。
|
||||
|
||||
代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。
|
||||
|
||||
## 监督
|
||||
|
||||
辱骂、骚扰或其他不可接受的行为可通过[info@rustdesk.com](mailto:info@rustdesk.com)向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。
|
||||
|
||||
所有社区领袖都有义务尊重任何事件报告者的隐私和安全。
|
||||
|
||||
## 处理方针
|
||||
|
||||
社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式:
|
||||
|
||||
### 1. 纠正
|
||||
|
||||
**社区影响**: 使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。
|
||||
|
||||
**处理意见**: 由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。
|
||||
|
||||
### 2. 警告
|
||||
|
||||
**社区影响**: 单个或一系列违规行为。
|
||||
|
||||
**处理意见**: 警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。
|
||||
|
||||
### 3. 临时封禁
|
||||
|
||||
**社区影响**: 严重违反社区准则,包括持续的不当行为。
|
||||
|
||||
**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。
|
||||
|
||||
### 4. 永久封禁
|
||||
|
||||
**社区影响**: 行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。
|
||||
|
||||
**处理意见**: 永久禁止在社区内进行任何形式的公开互动。
|
||||
|
||||
## 参见
|
||||
|
||||
本行为准则改编自[参与者公约][homepage]2.0 版, 参见
|
||||
[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
指导方针借鉴自[Mozilla纪检分级][Mozilla CoC].
|
||||
|
||||
有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译参见[https://www.contributor-covenant.org/translations][translations]。
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
32
docs/CONTRIBUTING-ZH.md
Normal file
32
docs/CONTRIBUTING-ZH.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 为RustDesk做贡献
|
||||
|
||||
Rust欢迎每一位贡献者,如果您有意向为我们做出贡献,请遵循以下指南:
|
||||
|
||||
## 贡献方式
|
||||
|
||||
对 RustDesk 或其依赖项的贡献需要通过 GitHub 的 Pull Request (PR) 的形式提交。每个 PR 都会由核心贡献者(即有权限合并代码的人)进行审核,审核通过后代码会合并到主分支,或者您会收到需要修改的反馈。所有贡献者,包括核心贡献者,提交的代码都应遵循此流程。
|
||||
|
||||
如果您希望处理某个问题,请先在对应的 GitHub issue 下发表评论,声明您将处理该问题,以避免该问题被多位贡献者重复处理。
|
||||
|
||||
## PR 注意事项
|
||||
|
||||
- 从 master 分支创建一个新的分支,并在提交PR之前,如果需要,将您的分支 变基(rebase) 到最新的 master 分支。如果您的分支无法顺利合并到 master 分支,您可能会被要求更新您的代码。
|
||||
|
||||
- 每次提交的改动应该尽可能少,并且要保证每次提交的代码都是正确的(即每个 commit 都应能成功编译并通过测试)。
|
||||
|
||||
- 每个提交都应附有开发者证书签名(http://developercertificate.org), 表明您(以及您的雇主,若适用)同意遵守项目[许可证条款](../LICENCE)。在使用 git 提交代码时,可以通过在 `git commit` 时使用 `-s` 选项加入签名
|
||||
|
||||
- 如果您的 PR 未被及时审核,或需要指定的人员进行审核,您可以通过在 PR 或评论中 @ 提到相关审核者,以及发送[电子邮件](mailto:info@rustdesk.com)的方式请求审核。
|
||||
|
||||
- 请为修复的 bug 或新增的功能添加相应的测试用例。
|
||||
|
||||
有关具体的 git 使用说明,请参考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||
|
||||
## 行为准则
|
||||
|
||||
请遵守项目的[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。
|
||||
|
||||
|
||||
## 沟通渠道
|
||||
|
||||
RustDesk 的贡献者主要通过 [Discord](https://discord.gg/nDceKgxnkV) 进行交流。
|
||||
@@ -1,60 +1,73 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - あなたのためのリモートデスクトップ"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-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をあなたの母国語に翻訳するために、あなたの助けが必要です。</b>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
|
||||
<b>READMEや<a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>、 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a>の翻訳者を歓迎します!</b>
|
||||
</p>
|
||||
|
||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。
|
||||
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを作成する](https://github.com/rustdesk/rustdesk-server-demo)こともできます。
|
||||
|
||||

|
||||
|
||||
RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。
|
||||
RustDeskは皆さんの貢献を歓迎します。
|
||||
貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。
|
||||
|
||||
[**RustDeskはどの様に動くのか?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
[**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**パッケージのダウンロード**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**ナイトリービルド**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="F-Droidで入手する"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## 依存関係
|
||||
|
||||
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。
|
||||
デスクトップ版ではGUIにFlutterまたはSciter(非推奨)を使用しますが、チュートリアルでは分かりやすく、簡単なSciterのみを対象に解説しています。Flutterでのビルド方法については[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)をご覧ください。
|
||||
|
||||
Sciter dynamic libraryを事前にダウンロードしてください。
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
モバイル版はFlutterを利用します。デスクトップ版もSciterからFlutterへマイグレーション予定です。
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## ビルド手順
|
||||
|
||||
- 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
|
||||
|
||||
- run `cargo run`
|
||||
- [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
|
||||
|
||||
- `cargo run`を実行します。
|
||||
|
||||
## [ビルド](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Linuxでのビルド手順
|
||||
## Linuxでのビルド方法
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
```
|
||||
|
||||
### 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)
|
||||
@@ -69,7 +82,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Install vcpkg
|
||||
### vcpkgのインストール
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
@@ -81,7 +94,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### Fix libvpx (For Fedora)
|
||||
### libvpxの修正 (Fedoraのみ)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
@@ -107,9 +120,9 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Dockerでビルドする方法
|
||||
## Dockerでのビルド方法
|
||||
|
||||
リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。
|
||||
リポジトリをクローンし、Dockerコンテナを構築します:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
@@ -117,44 +130,50 @@ 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
|
||||
```
|
||||
このコマンドはRustDeskをビルドする度に実行する必要があります。
|
||||
|
||||
なお、最初のビルドでは、依存関係がキャッシュされるまで時間がかかることがありますが、その後のビルドではより速くなります。さらに、ビルドコマンドに別の引数を指定する必要がある場合は、コマンドの最後にある `<OPTIONAL-ARGS>` の位置で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に
|
||||
`--release` を実行します。できあがった実行ファイルは、システムのターゲット・フォルダに格納され、次のコマンドで実行できます。
|
||||
初回ビルドは時間がかかるかもしれませんが、2回目以降は依存関係がキャッシュされるため、ビルドにかかる時間が短くなります。
|
||||
ビルドコマンドに追加の引数を指定する必要がある場合は、コマンドの最後(`<OPTIONAL-ARGS>`の位置)で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に `--release` を追記し実行します。ビルドされた実行ファイルはあなたのシステムのターゲットフォルダに保存され、下記のコマンドで実行することができます。
|
||||
|
||||
デバッグビルドを起動する場合:
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
あるいは、リリース用の実行ファイルを実行している場合:
|
||||
リリースビルドを起動する場合:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。
|
||||
コマンドをRustDeskリポジトリのルートから実行していることを確認してください。また、`install` や `run` などの他のcargoサブコマンドは、ホストではなくコンテナ内でプログラムをインストール、実行するため、現在の方法ではサポートされていません。
|
||||
|
||||
## ファイル構造
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、設定、tcp/udpラッパー、protobuf、ファイル転送に利用されるfs関数やその他のユーティリティ関数
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウスコントロール
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: オーディオ/クリップボード/入力/ビデオサービス、ネットワーク接続
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウス操作
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS向けのファイルのコピーと貼り付けの実装
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 廃止された Sciter UI (非推奨)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**:
|
||||
オーディオ/クリップボード/入力/ビデオ サービスとネットワーク接続
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う。
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
|
||||
|
||||
## スナップショット
|
||||
## スクリーンショット
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
@@ -164,3 +164,7 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
|
||||

|
||||
|
||||

|
||||
|
||||
## [Serwery publiczne](#public-servers)
|
||||
|
||||
RustDesk jest obsługiwany przez bezpłatne serwer w Unii Europejskiej, uprzejmie dostarczony przez [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<p align="center">
|
||||
<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="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>
|
||||
<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-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
|
||||
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</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)
|
||||
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
@@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
@@ -158,18 +156,22 @@ target/release/rustdesk
|
||||
- **[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)**: однорангове з'єднання
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове зʼєднання
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
|
||||
|
||||
## Знімки
|
||||
## Знімки екрана
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## [Публічні сервери](#публічні-сервери)
|
||||
|
||||
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn"><br>
|
||||
<a href="#free-public-servers">Máy chủ</a> •
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Server</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Cấu trúc tệp tin</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và <a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b>
|
||||
<b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
|
||||
</p>
|
||||
|
||||
Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
**RustDesk** luôn hoan nghênh mọi đóng góp từ mọi người. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu.
|
||||
|
||||
[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
|
||||
[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
@@ -29,28 +31,25 @@ Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để
|
||||
|
||||
## Dependencies
|
||||
|
||||
Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bạn cần tự tải về thư viện sciter.
|
||||
Phiên bản máy tính sử dụng __Flutter__ hoặc __Sciter__ (đã lỗi thời) cho giao diện người dùng (GUI). Hướng dẫn này chỉ áp dụng cho phiên bản Sciter, vì nó thân thiện và dễ bắt đầu hơn. Hãy kiểm tra [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) của chúng tôi để xây dựng phiên bản Flutter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
Vui lòng tự tải thư viện `Sciter` về máy theo hướng dẫn cho từng hệ điều hành.
|
||||
|
||||
Phiên bản cho điện thoại sử dụng Flutter. Chúng tôi sẽ chuyển sang sử dụng Flutter thay cho Sciter cho phiên bản máy tính.
|
||||
[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)
|
||||
|
||||
## Cách để build
|
||||
## Các bước build cơ bản
|
||||
|
||||
- Chuẩn bị môi trường phát triển Rust và môi trường build C++
|
||||
- Chuẩn bị môi trường phát triển Rust và môi trường biên dịch C++
|
||||
|
||||
- Tải và cài [vcpkg](https://github.com/microsoft/vcpkg), và đặt biến môi trường `VCPKG_ROOT` sao cho đúng.
|
||||
|
||||
- Đối với Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||
- Đối với Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
||||
- Tải và cài đặt [`vcpkg`](https://github.com/microsoft/vcpkg), và thiết lập biến môi trường `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`
|
||||
- Chạy lệnh `cargo run`
|
||||
|
||||
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Cách để build cho Linux
|
||||
## Cách build cho Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
@@ -70,7 +69,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Cách cài vcpkg
|
||||
### Cách cài đặt `vcpkg`
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
@@ -82,7 +81,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### Cách sửa lỗi libvpx (Dành cho hệ điều hành Fedora)
|
||||
### Cách sửa lỗi `libvpx` (Dành cho hệ điều hành Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
@@ -95,7 +94,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Cách build
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
@@ -108,9 +107,9 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Cách để build sử dụng Docker
|
||||
## Cách build bằ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:
|
||||
Bắt đầu bằng cách sao chép repo này về máy tính của bạn và tạo Docker container:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
@@ -118,37 +117,37 @@ cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Rồi mỗi khi bạn chạy ứng dụng, thì hãy chạy lệnh này:
|
||||
Sau đó, mỗi khi bạn chạy ứng dụng, thì hãy chạy dòng lệnh sau:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Chú ý: Lần build đầu tiên có thể sẽ mất lâu hơn truớc khi các dependecies đuợc lưu lại, những lần build sau sẽ nhanh hơn. Hơn nũa, nếu bạn cần cung cấp các cài đặt lệnh khác cho lệnh build, bạn có thể đặt những cài đặt lệnh này vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh ‘--release’. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
|
||||
Lưu ý rằng **lần build đầu tiên có thể mất thời gian hơn trước khi các dependencies được lưu vào bộ nhớ cache**, nhưng các lần build sau sẽ nhanh hơn. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên với tùy chọn `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh:
|
||||
Nếu bạn đang chạy bản build được tối ưu hóa, thì bạn có thể chạy với lệnh:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Hãy đảm bảo là bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn.
|
||||
Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác của **cargo** như **install** hoặc **run** hiện không được hỗ trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính của bạn.
|
||||
|
||||
## Cấu trúc tệp tin
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: điều khiển máy tính/chuột trên các nền tảng khác nhau
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed.
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối với một peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành cho điện thoại
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
|
||||
|
||||
## Snapshot
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
</p>
|
||||
|
||||
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
|
||||
|
||||

|
||||
|
||||
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md).
|
||||
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
@@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m
|
||||
|
||||
## 依赖
|
||||
|
||||
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。
|
||||
桌面版本使用 Flutter 或 Sciter(已弃用)作为 GUI,本教程仅适用于 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) |
|
||||
@@ -133,8 +135,8 @@ docker build -t "rustdesk-builder" . # 构建容器
|
||||
```
|
||||
在Dockerfile的RUN apt update之前插入两行:
|
||||
|
||||
RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list
|
||||
RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list
|
||||
RUN sed -i "s|deb.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list && \
|
||||
sed -i "s|security.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list
|
||||
```
|
||||
|
||||
2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码:
|
||||
@@ -207,12 +209,13 @@ target/release/rustdesk
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI(已弃用)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
|
||||
|
||||
## 截图
|
||||
|
||||
@@ -8,17 +8,31 @@
|
||||
"modules": [
|
||||
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
|
||||
"xdotool.json",
|
||||
{
|
||||
"name": "pam",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"./configure --disable-selinux --prefix=/app && make -j4 install"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
|
||||
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rustdesk",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"bsdtar -zxvf rustdesk-1.2.4.deb",
|
||||
"bsdtar -zxvf rustdesk.deb",
|
||||
"tar -xvf ./data.tar.xz",
|
||||
"cp -r ./usr/* /app/",
|
||||
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
|
||||
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
|
||||
"mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
|
||||
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
|
||||
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
|
||||
],
|
||||
@@ -26,7 +40,7 @@
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../rustdesk-1.2.4.deb"
|
||||
"path": "./rustdesk.deb"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import com.google.protobuf.gradle.*
|
||||
plugins {
|
||||
id "com.google.protobuf" version "0.9.4"
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
@@ -17,11 +20,6 @@ if (localPropertiesFile.exists()) {
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
@@ -32,10 +30,6 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
@@ -57,7 +51,7 @@ protobuf {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
||||
@@ -105,7 +99,6 @@ flutter {
|
||||
dependencies {
|
||||
implementation "androidx.media:media:1.6.0"
|
||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } }
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "768133699366",
|
||||
"firebase_url": "https://rustdesk.firebaseio.com",
|
||||
"project_id": "rustdesk",
|
||||
"storage_bucket": "rustdesk.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
|
||||
"android_client_info": {
|
||||
"package_name": "com.carriez.flutter_hbb"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package="com.carriez.flutter_hbb">
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
@@ -14,6 +15,13 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<!-- https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs -->
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="RustDesk"
|
||||
@@ -80,6 +88,11 @@
|
||||
android:name=".MainService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<service
|
||||
android:name=".FloatingWindowService"
|
||||
android:enabled="true" />
|
||||
|
||||
<!--
|
||||
Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.media.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.projection.MediaProjection
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.ActivityCompat
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
||||
const val AUDIO_SAMPLE_RATE = 48000
|
||||
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||
|
||||
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
||||
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
|
||||
|
||||
private var audioRecorder: AudioRecord? = null
|
||||
private var audioReader: AudioReader? = null
|
||||
private var minBufferSize = 0
|
||||
private var audioRecordStat = false
|
||||
private var audioThread: Thread? = null
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return false
|
||||
}
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Log.d(logTag, "createAudioRecorder failed, no RECORD_AUDIO permission")
|
||||
return false
|
||||
}
|
||||
|
||||
var builder = AudioRecord.Builder()
|
||||
.setAudioFormat(
|
||||
AudioFormat.Builder()
|
||||
.setEncoding(AUDIO_ENCODING)
|
||||
.setSampleRate(AUDIO_SAMPLE_RATE)
|
||||
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
||||
);
|
||||
if (inVoiceCall) {
|
||||
builder.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
|
||||
} else {
|
||||
mediaProjection?.let {
|
||||
var apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build();
|
||||
builder.setAudioPlaybackCaptureConfig(apcc);
|
||||
} ?: let {
|
||||
Log.d(logTag, "createAudioRecorder failed, mediaProjection null")
|
||||
return false
|
||||
}
|
||||
}
|
||||
audioRecorder = builder.build()
|
||||
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun checkAudioReader() {
|
||||
if (audioReader != null && minBufferSize != 0) {
|
||||
return
|
||||
}
|
||||
// read f32 to byte , length * 4
|
||||
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||
AUDIO_SAMPLE_RATE,
|
||||
AUDIO_CHANNEL_MASK,
|
||||
AUDIO_ENCODING
|
||||
)
|
||||
if (minBufferSize == 0) {
|
||||
Log.d(logTag, "get min buffer size fail!")
|
||||
return
|
||||
}
|
||||
audioReader = AudioReader(minBufferSize, 4)
|
||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun startAudioRecorder() {
|
||||
checkAudioReader()
|
||||
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||
try {
|
||||
FFI.setFrameRawEnable("audio", true)
|
||||
audioRecorder!!.startRecording()
|
||||
audioRecordStat = true
|
||||
audioThread = thread {
|
||||
while (audioRecordStat) {
|
||||
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||
FFI.onAudioFrameUpdate(it)
|
||||
}
|
||||
}
|
||||
// let's release here rather than onDestroy to avoid threading issue
|
||||
audioRecorder?.release()
|
||||
audioRecorder = null
|
||||
minBufferSize = 0
|
||||
FFI.setFrameRawEnable("audio", false)
|
||||
Log.d(logTag, "Exit audio thread")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(logTag, "startAudioRecorder fail:$e")
|
||||
}
|
||||
} else {
|
||||
Log.d(logTag, "startAudioRecorder fail")
|
||||
}
|
||||
}
|
||||
|
||||
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||
if (!isSupportVoiceCall()) {
|
||||
return false
|
||||
}
|
||||
// No need to check if video or audio is started here.
|
||||
if (!switchToVoiceCall(mediaProjection)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
||||
// Return true if not supported, because is was not started.
|
||||
if (!isSupportVoiceCall()) {
|
||||
return true
|
||||
}
|
||||
if (isVideoStart()) {
|
||||
switchOutVoiceCall(mediaProjection)
|
||||
}
|
||||
tryReleaseAudio()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun switchToVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||
audioRecorder?.let {
|
||||
if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
audioThread = null
|
||||
|
||||
if (!createAudioRecorder(true, mediaProjection)) {
|
||||
Log.e(logTag, "createAudioRecorder fail")
|
||||
return false
|
||||
}
|
||||
startAudioRecorder()
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||
audioRecorder?.let {
|
||||
if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
|
||||
if (!createAudioRecorder(false, mediaProjection)) {
|
||||
Log.e(logTag, "createAudioRecorder fail")
|
||||
return false
|
||||
}
|
||||
startAudioRecorder()
|
||||
return true
|
||||
}
|
||||
|
||||
fun tryReleaseAudio() {
|
||||
if (isAudioStart() || isVideoStart()) {
|
||||
return
|
||||
}
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
audioThread = null
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
Log.d(logTag, "destroy audio record handle")
|
||||
|
||||
audioRecordStat = false
|
||||
audioThread?.join()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||
import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import com.caverock.androidsvg.SVG
|
||||
import ffi.FFI
|
||||
import kotlin.math.abs
|
||||
|
||||
class FloatingWindowService : Service(), View.OnTouchListener {
|
||||
|
||||
private lateinit var windowManager: WindowManager
|
||||
private lateinit var layoutParams: WindowManager.LayoutParams
|
||||
private lateinit var floatingView: ImageView
|
||||
private lateinit var originalDrawable: Drawable
|
||||
private lateinit var leftHalfDrawable: Drawable
|
||||
private lateinit var rightHalfDrawable: Drawable
|
||||
|
||||
private var dragging = false
|
||||
private var lastDownX = 0f
|
||||
private var lastDownY = 0f
|
||||
private var viewCreated = false;
|
||||
private var keepScreenOn = KeepScreenOn.DURING_CONTROLLED
|
||||
|
||||
companion object {
|
||||
private val logTag = "floatingService"
|
||||
private var firstCreate = true
|
||||
private var viewWidth = 120
|
||||
private var viewHeight = 120
|
||||
private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed
|
||||
private const val MAX_VIEW_SIZE = 320
|
||||
private var viewUntouchable = false
|
||||
private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed
|
||||
private var customSvg = ""
|
||||
private var lastLayoutX = 0
|
||||
private var lastLayoutY = 0
|
||||
private var lastOrientation = Configuration.ORIENTATION_UNDEFINED
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||
try {
|
||||
if (firstCreate) {
|
||||
firstCreate = false
|
||||
onFirstCreate(windowManager)
|
||||
}
|
||||
Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg")
|
||||
createView(windowManager)
|
||||
handler.postDelayed(runnable, 1000)
|
||||
Log.d(logTag, "onCreate success")
|
||||
} catch (e: Exception) {
|
||||
Log.d(logTag, "onCreate failed: $e")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (viewCreated) {
|
||||
windowManager.removeView(floatingView)
|
||||
}
|
||||
handler.removeCallbacks(runnable)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun createView(windowManager: WindowManager) {
|
||||
floatingView = ImageView(this)
|
||||
viewCreated = true
|
||||
originalDrawable = resources.getDrawable(R.drawable.floating_window, null)
|
||||
if (customSvg.isNotEmpty()) {
|
||||
try {
|
||||
val svg = SVG.getFromString(customSvg)
|
||||
Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}");
|
||||
// This make the svg render clear
|
||||
svg.documentWidth = viewWidth * 1f
|
||||
svg.documentHeight = viewHeight * 1f
|
||||
originalDrawable = svg.renderToPicture().let {
|
||||
BitmapDrawable(
|
||||
resources,
|
||||
Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888)
|
||||
.also { bitmap ->
|
||||
it.draw(Canvas(bitmap))
|
||||
})
|
||||
}
|
||||
floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
Log.d(logTag, "custom svg loaded")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
val originalBitmap = Bitmap.createBitmap(
|
||||
originalDrawable.intrinsicWidth,
|
||||
originalDrawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(originalBitmap)
|
||||
originalDrawable.setBounds(
|
||||
0,
|
||||
0,
|
||||
originalDrawable.intrinsicWidth,
|
||||
originalDrawable.intrinsicHeight
|
||||
)
|
||||
originalDrawable.draw(canvas)
|
||||
val leftHalfBitmap = Bitmap.createBitmap(
|
||||
originalBitmap,
|
||||
0,
|
||||
0,
|
||||
originalDrawable.intrinsicWidth / 2,
|
||||
originalDrawable.intrinsicHeight
|
||||
)
|
||||
val rightHalfBitmap = Bitmap.createBitmap(
|
||||
originalBitmap,
|
||||
originalDrawable.intrinsicWidth / 2,
|
||||
0,
|
||||
originalDrawable.intrinsicWidth / 2,
|
||||
originalDrawable.intrinsicHeight
|
||||
)
|
||||
leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap)
|
||||
rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap)
|
||||
|
||||
floatingView.setImageDrawable(rightHalfDrawable)
|
||||
floatingView.setOnTouchListener(this)
|
||||
floatingView.alpha = viewTransparency * 1f
|
||||
|
||||
var flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
|
||||
if (viewUntouchable || viewTransparency == 0f) {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
}
|
||||
layoutParams = WindowManager.LayoutParams(
|
||||
viewWidth / 2,
|
||||
viewHeight,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
|
||||
flags,
|
||||
PixelFormat.TRANSLUCENT
|
||||
)
|
||||
|
||||
layoutParams.gravity = Gravity.TOP or Gravity.START
|
||||
layoutParams.x = lastLayoutX
|
||||
layoutParams.y = lastLayoutY
|
||||
|
||||
val keepScreenOnOption = FFI.getLocalOption("keep-screen-on").lowercase()
|
||||
keepScreenOn = when (keepScreenOnOption) {
|
||||
"never" -> KeepScreenOn.NEVER
|
||||
"service-on" -> KeepScreenOn.SERVICE_ON
|
||||
else -> KeepScreenOn.DURING_CONTROLLED
|
||||
}
|
||||
Log.d(logTag, "keepScreenOn option: $keepScreenOnOption, value: $keepScreenOn")
|
||||
updateKeepScreenOnLayoutParams()
|
||||
|
||||
windowManager.addView(floatingView, layoutParams)
|
||||
moveToScreenSide()
|
||||
}
|
||||
|
||||
private fun onFirstCreate(windowManager: WindowManager) {
|
||||
val wh = getScreenSize(windowManager)
|
||||
val w = wh.first
|
||||
val h = wh.second
|
||||
// size
|
||||
FFI.getLocalOption("floating-window-size").let {
|
||||
if (it.isNotEmpty()) {
|
||||
try {
|
||||
val size = it.toInt()
|
||||
if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) {
|
||||
viewWidth = size
|
||||
viewHeight = size
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
// untouchable
|
||||
viewUntouchable = FFI.getLocalOption("floating-window-untouchable") == "Y"
|
||||
// transparency
|
||||
FFI.getLocalOption("floating-window-transparency").let {
|
||||
if (it.isNotEmpty()) {
|
||||
try {
|
||||
val transparency = it.toInt()
|
||||
if (transparency in 0..10) {
|
||||
viewTransparency = transparency * 1f / 10
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
// custom svg
|
||||
FFI.getLocalOption("floating-window-svg").let {
|
||||
if (it.isNotEmpty()) {
|
||||
customSvg = it
|
||||
}
|
||||
}
|
||||
// position
|
||||
lastLayoutX = 0
|
||||
lastLayoutY = (wh.second - viewHeight) / 2
|
||||
lastOrientation = resources.configuration.orientation
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun performClick() {
|
||||
showPopupMenu()
|
||||
}
|
||||
|
||||
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||
when (event?.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
dragging = false
|
||||
lastDownX = event.rawX
|
||||
lastDownY = event.rawY
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val clickDragTolerance = 10f
|
||||
if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) {
|
||||
performClick()
|
||||
} else {
|
||||
moveToScreenSide()
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val dx = event.rawX - lastDownX
|
||||
val dy = event.rawY - lastDownY
|
||||
// ignore too small fist start moving(some time is click)
|
||||
if (!dragging && dx*dx+dy*dy < 25) {
|
||||
return false
|
||||
}
|
||||
dragging = true
|
||||
layoutParams.x = event.rawX.toInt()
|
||||
layoutParams.y = event.rawY.toInt()
|
||||
layoutParams.width = viewWidth
|
||||
floatingView.setImageDrawable(originalDrawable)
|
||||
windowManager.updateViewLayout(view, layoutParams)
|
||||
lastLayoutX = layoutParams.x
|
||||
lastLayoutY = layoutParams.y
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun moveToScreenSide(center: Boolean = false) {
|
||||
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||
val wh = getScreenSize(windowManager)
|
||||
val w = wh.first
|
||||
if (layoutParams.x < w / 2) {
|
||||
layoutParams.x = 0
|
||||
floatingView.setImageDrawable(rightHalfDrawable)
|
||||
} else {
|
||||
layoutParams.x = w - viewWidth / 2
|
||||
floatingView.setImageDrawable(leftHalfDrawable)
|
||||
}
|
||||
if (center) {
|
||||
layoutParams.y = (wh.second - viewHeight) / 2
|
||||
}
|
||||
layoutParams.width = viewWidth / 2
|
||||
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||
lastLayoutX = layoutParams.x
|
||||
lastLayoutY = layoutParams.y
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (newConfig.orientation != lastOrientation) {
|
||||
lastOrientation = newConfig.orientation
|
||||
val wh = getScreenSize(windowManager)
|
||||
Log.d(logTag, "orientation: $lastOrientation, screen size: ${wh.first} x ${wh.second}")
|
||||
val newW = wh.first
|
||||
val newH = wh.second
|
||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
// Proportional change
|
||||
layoutParams.x = (layoutParams.x.toFloat() / newH.toFloat() * newW.toFloat()).toInt()
|
||||
layoutParams.y = (layoutParams.y.toFloat() / newW.toFloat() * newH.toFloat()).toInt()
|
||||
}
|
||||
moveToScreenSide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPopupMenu() {
|
||||
val popupMenu = PopupMenu(this, floatingView)
|
||||
val idShowRustDesk = 0
|
||||
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
|
||||
// For host side, clipboard sync
|
||||
val idSyncClipboard = 1
|
||||
val isClipboardListenerEnabled = MainActivity.rdClipboardManager?.isListening ?: false
|
||||
if (isClipboardListenerEnabled) {
|
||||
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
|
||||
}
|
||||
val idStopService = 2
|
||||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
idShowRustDesk -> {
|
||||
openMainActivity()
|
||||
true
|
||||
}
|
||||
idSyncClipboard -> {
|
||||
syncClipboard()
|
||||
true
|
||||
}
|
||||
idStopService -> {
|
||||
stopMainService()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
popupMenu.setOnDismissListener {
|
||||
moveToScreenSide()
|
||||
}
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
|
||||
private fun openMainActivity() {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
|
||||
)
|
||||
try {
|
||||
pendingIntent.send()
|
||||
} catch (e: PendingIntent.CanceledException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncClipboard() {
|
||||
MainActivity.rdClipboardManager?.syncClipboard(false)
|
||||
}
|
||||
|
||||
private fun stopMainService() {
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
|
||||
}
|
||||
|
||||
enum class KeepScreenOn {
|
||||
NEVER,
|
||||
DURING_CONTROLLED,
|
||||
SERVICE_ON,
|
||||
}
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val runnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (updateKeepScreenOnLayoutParams()) {
|
||||
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||
}
|
||||
handler.postDelayed(this, 1000) // 1000 milliseconds = 1 second
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateKeepScreenOnLayoutParams(): Boolean {
|
||||
val oldOn = layoutParams.flags and FLAG_KEEP_SCREEN_ON != 0
|
||||
val newOn = keepScreenOn == KeepScreenOn.SERVICE_ON || (keepScreenOn == KeepScreenOn.DURING_CONTROLLED && MainService.isStart)
|
||||
if (oldOn != newOn) {
|
||||
Log.d(logTag, "change keep screen on to $newOn")
|
||||
if (newOn) {
|
||||
layoutParams.flags = layoutParams.flags or FLAG_KEEP_SCREEN_ON
|
||||
} else {
|
||||
layoutParams.flags = layoutParams.flags and FLAG_KEEP_SCREEN_ON.inv()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ import android.widget.EditText
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.view.KeyEvent as KeyEventAndroid
|
||||
import android.graphics.Rect
|
||||
import android.media.AudioManager
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||
@@ -75,6 +77,8 @@ class InputService : AccessibilityService() {
|
||||
|
||||
private var fakeEditTextForTextStateCalculation: EditText? = null
|
||||
|
||||
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
||||
val x = max(0, _x)
|
||||
@@ -276,24 +280,36 @@ class InputService : AccessibilityService() {
|
||||
|
||||
var textToCommit: String? = null
|
||||
|
||||
if (keyboardMode == KeyboardMode.Legacy) {
|
||||
if (keyEvent.hasChr() && keyEvent.getDown()) {
|
||||
// [down] indicates the key's state(down or up).
|
||||
// [press] indicates a click event(down and up).
|
||||
// https://github.com/rustdesk/rustdesk/blob/3a7594755341f023f56fa4b6a43b60d6b47df88d/flutter/lib/models/input_model.dart#L688
|
||||
if (keyEvent.hasSeq()) {
|
||||
textToCommit = keyEvent.getSeq()
|
||||
} else if (keyboardMode == KeyboardMode.Legacy) {
|
||||
if (keyEvent.hasChr() && (keyEvent.getDown() || keyEvent.getPress())) {
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
||||
|
||||
var ke: KeyEventAndroid? = null
|
||||
if (Build.VERSION.SDK_INT < 33 || textToCommit == null) {
|
||||
ke = KeyEventConverter.toAndroidKeyEvent(keyEvent)
|
||||
}
|
||||
ke?.let { event ->
|
||||
if (tryHandleVolumeKeyEvent(event)) {
|
||||
return
|
||||
} else if (tryHandlePowerKeyEvent(event)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
getInputMethod()?.let { inputMethod ->
|
||||
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
||||
@@ -302,8 +318,12 @@ class InputService : AccessibilityService() {
|
||||
inputConnection.commitText(text, 1, null)
|
||||
}
|
||||
} else {
|
||||
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event ->
|
||||
ke?.let { event ->
|
||||
inputConnection.sendKeyEvent(event)
|
||||
if (keyEvent.getPress()) {
|
||||
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
|
||||
inputConnection.sendKeyEvent(actionUpEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,12 +331,16 @@ class InputService : AccessibilityService() {
|
||||
} else {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
handler.post {
|
||||
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event ->
|
||||
ke?.let { event ->
|
||||
val possibleNodes = possibleAccessibiltyNodes()
|
||||
Log.d(logTag, "possibleNodes:$possibleNodes")
|
||||
for (item in possibleNodes) {
|
||||
val success = trySendKeyEvent(event, item, textToCommit)
|
||||
if (success) {
|
||||
if (keyEvent.getPress()) {
|
||||
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
|
||||
trySendKeyEvent(actionUpEvent, item, textToCommit)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -325,6 +349,43 @@ class InputService : AccessibilityService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryHandleVolumeKeyEvent(event: KeyEventAndroid): Boolean {
|
||||
when (event.keyCode) {
|
||||
KeyEventAndroid.KEYCODE_VOLUME_UP -> {
|
||||
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||
volumeController.raiseVolume(null, true, AudioManager.STREAM_SYSTEM)
|
||||
}
|
||||
return true
|
||||
}
|
||||
KeyEventAndroid.KEYCODE_VOLUME_DOWN -> {
|
||||
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||
volumeController.lowerVolume(null, true, AudioManager.STREAM_SYSTEM)
|
||||
}
|
||||
return true
|
||||
}
|
||||
KeyEventAndroid.KEYCODE_VOLUME_MUTE -> {
|
||||
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||
volumeController.toggleMute(true, AudioManager.STREAM_SYSTEM)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryHandlePowerKeyEvent(event: KeyEventAndroid): Boolean {
|
||||
if (event.keyCode == KeyEventAndroid.KEYCODE_POWER) {
|
||||
// Perform power dialog action when action is up
|
||||
if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
|
||||
if (node == null) {
|
||||
return
|
||||
@@ -422,7 +483,7 @@ class InputService : AccessibilityService() {
|
||||
return linkedList
|
||||
}
|
||||
|
||||
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
||||
private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
||||
node.refresh()
|
||||
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
||||
this.fakeEditTextForTextStateCalculation?.setText(null)
|
||||
@@ -487,10 +548,10 @@ class InputService : AccessibilityService() {
|
||||
|
||||
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
||||
it.onPreDraw()
|
||||
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
|
||||
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||
val succ = it.onKeyDown(event.getKeyCode(), event)
|
||||
Log.d(logTag, "onKeyDown $succ")
|
||||
} else if (event.action == android.view.KeyEvent.ACTION_UP) {
|
||||
} else if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||
val success = it.onKeyUp(event.getKeyCode(), event)
|
||||
Log.d(logTag, "keyup $success")
|
||||
} else {}
|
||||
|
||||
@@ -31,7 +31,7 @@ object KeyEventConverter {
|
||||
}
|
||||
|
||||
var action = 0
|
||||
if (keyEventProto.getDown()) {
|
||||
if (keyEventProto.getDown() || keyEventProto.getPress()) {
|
||||
action = KeyEvent.ACTION_DOWN
|
||||
} else {
|
||||
action = KeyEvent.ACTION_UP
|
||||
@@ -112,6 +112,10 @@ object KeyEventConverter {
|
||||
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
||||
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
||||
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
||||
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
|
||||
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
|
||||
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
ControlKey.Power -> KeyEvent.KEYCODE_POWER
|
||||
else -> 0 // Default to unknown.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,29 +7,49 @@ package com.carriez.flutter_hbb
|
||||
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
||||
*/
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.ClipboardManager
|
||||
import android.os.Bundle
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
||||
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
||||
import android.media.MediaCodecList
|
||||
import android.media.MediaFormat
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
companion object {
|
||||
var flutterMethodChannel: MethodChannel? = null
|
||||
private var _rdClipboardManager: RdClipboardManager? = null
|
||||
val rdClipboardManager: RdClipboardManager?
|
||||
get() = _rdClipboardManager;
|
||||
}
|
||||
|
||||
private val channelTag = "mChannel"
|
||||
private val logTag = "mMainActivity"
|
||||
private var mainService: MainService? = null
|
||||
|
||||
private var isAudioStart = false
|
||||
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
if (MainService.isReady) {
|
||||
@@ -42,6 +62,7 @@ class MainActivity : FlutterActivity() {
|
||||
channelTag
|
||||
)
|
||||
initFlutterChannel(flutterMethodChannel!!)
|
||||
thread { setCodecInfo() }
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -69,11 +90,20 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (_rdClipboardManager == null) {
|
||||
_rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
|
||||
FFI.setClipboardManager(_rdClipboardManager!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.e(logTag, "onDestroy")
|
||||
mainService?.let {
|
||||
unbindService(serviceConnection)
|
||||
}
|
||||
rdClipboardManager?.rustEnableServiceClipboard(false)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -217,10 +247,168 @@ class MainActivity : FlutterActivity() {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
GET_VALUE -> {
|
||||
if (call.arguments is String) {
|
||||
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
|
||||
result.success(isSupportVoiceCall())
|
||||
} else {
|
||||
result.error("-1", "No such key", null)
|
||||
}
|
||||
} else {
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
"on_voice_call_started" -> {
|
||||
onVoiceCallStarted()
|
||||
}
|
||||
"on_voice_call_closed" -> {
|
||||
onVoiceCallClosed()
|
||||
}
|
||||
else -> {
|
||||
result.error("-1", "No such method", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCodecInfo() {
|
||||
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||
val codecs = codecList.codecInfos
|
||||
val codecArray = JSONArray()
|
||||
|
||||
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
val wh = getScreenSize(windowManager)
|
||||
var w = wh.first
|
||||
var h = wh.second
|
||||
val align = 64
|
||||
w = (w + align - 1) / align * align
|
||||
h = (h + align - 1) / align * align
|
||||
codecs.forEach { codec ->
|
||||
val codecObject = JSONObject()
|
||||
codecObject.put("name", codec.name)
|
||||
codecObject.put("is_encoder", codec.isEncoder)
|
||||
var hw: Boolean? = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
hw = codec.isHardwareAccelerated
|
||||
} else {
|
||||
// https://chromium.googlesource.com/external/webrtc/+/HEAD/sdk/android/src/java/org/webrtc/MediaCodecUtils.java#29
|
||||
// https://chromium.googlesource.com/external/webrtc/+/master/sdk/android/api/org/webrtc/HardwareVideoEncoderFactory.java#229
|
||||
if (listOf("OMX.google.", "OMX.SEC.", "c2.android").any { codec.name.startsWith(it, true) }) {
|
||||
hw = false
|
||||
} else if (listOf("c2.qti", "OMX.qcom.video", "OMX.Exynos", "OMX.hisi", "OMX.MTK", "OMX.Intel", "OMX.Nvidia").any { codec.name.startsWith(it, true) }) {
|
||||
hw = true
|
||||
}
|
||||
}
|
||||
if (hw != true) {
|
||||
return@forEach
|
||||
}
|
||||
codecObject.put("hw", hw)
|
||||
var mime_type = ""
|
||||
codec.supportedTypes.forEach { type ->
|
||||
if (listOf("video/avc", "video/hevc").contains(type)) { // "video/x-vnd.on2.vp8", "video/x-vnd.on2.vp9", "video/av01"
|
||||
mime_type = type;
|
||||
}
|
||||
}
|
||||
if (mime_type.isNotEmpty()) {
|
||||
codecObject.put("mime_type", mime_type)
|
||||
val caps = codec.getCapabilitiesForType(mime_type)
|
||||
if (codec.isEncoder) {
|
||||
// Encoder‘s max_height and max_width are interchangeable
|
||||
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
codecObject.put("min_width", caps.videoCapabilities.supportedWidths.lower)
|
||||
codecObject.put("max_width", caps.videoCapabilities.supportedWidths.upper)
|
||||
codecObject.put("min_height", caps.videoCapabilities.supportedHeights.lower)
|
||||
codecObject.put("max_height", caps.videoCapabilities.supportedHeights.upper)
|
||||
val surface = caps.colorFormats.contains(COLOR_FormatSurface);
|
||||
codecObject.put("surface", surface)
|
||||
val nv12 = caps.colorFormats.contains(COLOR_FormatYUV420SemiPlanar)
|
||||
codecObject.put("nv12", nv12)
|
||||
if (!(nv12 || surface)) {
|
||||
return@forEach
|
||||
}
|
||||
codecObject.put("min_bitrate", caps.videoCapabilities.bitrateRange.lower / 1000)
|
||||
codecObject.put("max_bitrate", caps.videoCapabilities.bitrateRange.upper / 1000)
|
||||
if (!codec.isEncoder) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
codecObject.put("low_latency", caps.isFeatureSupported(MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency))
|
||||
}
|
||||
}
|
||||
if (!codec.isEncoder) {
|
||||
return@forEach
|
||||
}
|
||||
codecArray.put(codecObject)
|
||||
}
|
||||
}
|
||||
val result = JSONObject()
|
||||
result.put("version", Build.VERSION.SDK_INT)
|
||||
result.put("w", w)
|
||||
result.put("h", h)
|
||||
result.put("codecs", codecArray)
|
||||
FFI.setCodecInfo(result.toString())
|
||||
}
|
||||
|
||||
private fun onVoiceCallStarted() {
|
||||
var ok = false
|
||||
mainService?.let {
|
||||
ok = it.onVoiceCallStarted()
|
||||
} ?: let {
|
||||
isAudioStart = true
|
||||
ok = audioRecordHandle.onVoiceCallStarted(null)
|
||||
}
|
||||
if (!ok) {
|
||||
// Rarely happens, So we just add log and msgbox here.
|
||||
Log.e(logTag, "onVoiceCallStarted fail")
|
||||
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to start voice call."))
|
||||
} else {
|
||||
Log.d(logTag, "onVoiceCallStarted success")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onVoiceCallClosed() {
|
||||
var ok = false
|
||||
mainService?.let {
|
||||
ok = it.onVoiceCallClosed()
|
||||
} ?: let {
|
||||
isAudioStart = false
|
||||
ok = audioRecordHandle.onVoiceCallClosed(null)
|
||||
}
|
||||
if (!ok) {
|
||||
// Rarely happens, So we just add log and msgbox here.
|
||||
Log.e(logTag, "onVoiceCallClosed fail")
|
||||
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to stop voice call."))
|
||||
} else {
|
||||
Log.d(logTag, "onVoiceCallClosed success")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
val disableFloatingWindow = FFI.getLocalOption("disable-floating-window") == "Y"
|
||||
if (!disableFloatingWindow && MainService.isReady) {
|
||||
startService(Intent(this, FloatingWindowService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
stopService(Intent(this, FloatingWindowService::class.java))
|
||||
}
|
||||
|
||||
// For client side
|
||||
// When swithing from other app to this app, try to sync clipboard.
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
rdClipboardManager?.syncClipboard(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
/**
|
||||
* Capture screen,get video and audio,send to rust.
|
||||
* Dispatch notifications
|
||||
@@ -52,27 +54,19 @@ const val NOTIFY_ID_OFFSET = 100
|
||||
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
|
||||
|
||||
// video const
|
||||
|
||||
const val MAX_SCREEN_SIZE = 1200
|
||||
|
||||
const val VIDEO_KEY_BIT_RATE = 1024_000
|
||||
const val VIDEO_KEY_FRAME_RATE = 30
|
||||
|
||||
// audio const
|
||||
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
||||
const val AUDIO_SAMPLE_RATE = 48000
|
||||
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||
|
||||
class MainService : Service() {
|
||||
|
||||
init {
|
||||
System.loadLibrary("rustdesk")
|
||||
}
|
||||
|
||||
@Keep
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
|
||||
fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
|
||||
// turn on screen with LIFT_DOWN when screen off
|
||||
if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
|
||||
if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
|
||||
if (wakeLock.isHeld) {
|
||||
Log.d(logTag, "Turn on Screen, WakeLock release")
|
||||
wakeLock.release()
|
||||
@@ -81,10 +75,10 @@ class MainService : Service() {
|
||||
wakeLock.acquire(5000)
|
||||
} else {
|
||||
when (kind) {
|
||||
"touch" -> {
|
||||
0 -> { // touch
|
||||
InputService.ctx?.onTouchInput(mask, x, y)
|
||||
}
|
||||
"mouse" -> {
|
||||
1 -> { // mouse
|
||||
InputService.ctx?.onMouseInput(mask, x, y)
|
||||
}
|
||||
else -> {
|
||||
@@ -109,6 +103,9 @@ class MainService : Service() {
|
||||
put("scale",SCREEN_INFO.scale)
|
||||
}.toString()
|
||||
}
|
||||
"is_start" -> {
|
||||
isStart.toString()
|
||||
}
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
@@ -141,10 +138,51 @@ class MainService : Service() {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
"update_voice_call_state" -> {
|
||||
try {
|
||||
val jsonObject = JSONObject(arg1)
|
||||
val id = jsonObject["id"] as Int
|
||||
val username = jsonObject["name"] as String
|
||||
val peerId = jsonObject["peer_id"] as String
|
||||
val inVoiceCall = jsonObject["in_voice_call"] as Boolean
|
||||
val incomingVoiceCall = jsonObject["incoming_voice_call"] as Boolean
|
||||
if (!inVoiceCall) {
|
||||
if (incomingVoiceCall) {
|
||||
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
||||
} else {
|
||||
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
|
||||
Log.e(logTag, "switchOutVoiceCall fail")
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to switch out voice call."))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
|
||||
Log.e(logTag, "switchToVoiceCall fail")
|
||||
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||
"type" to "custom-nook-nocancel-hasclose-error",
|
||||
"title" to "Voice call",
|
||||
"text" to "Failed to switch to voice call."))
|
||||
}
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
"stop_capture" -> {
|
||||
Log.d(logTag, "from rust:stop_capture")
|
||||
stopCapture()
|
||||
}
|
||||
"half_scale" -> {
|
||||
val halfScale = arg1.toBoolean()
|
||||
if (isHalfScale != halfScale) {
|
||||
isHalfScale = halfScale
|
||||
updateScreenInfo(resources.configuration.orientation)
|
||||
}
|
||||
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
@@ -156,38 +194,23 @@ class MainService : Service() {
|
||||
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
||||
|
||||
// jvm call rust
|
||||
private external fun init(ctx: Context)
|
||||
|
||||
/// When app start on boot, app_dir will not be passed from flutter
|
||||
/// so pass a app_dir here to rust server
|
||||
private external fun startServer(app_dir: String)
|
||||
private external fun startService()
|
||||
private external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||
private external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||
private external fun translateLocale(localeName: String, input: String): String
|
||||
private external fun refreshScreen()
|
||||
private external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
// private external fun sendVp9(data: ByteArray)
|
||||
|
||||
private fun translate(input: String): String {
|
||||
Log.d(logTag, "translate:$LOCAL_NAME")
|
||||
return translateLocale(LOCAL_NAME, input)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var _isReady = false // media permission ready status
|
||||
private var _isStart = false // screen capture start status
|
||||
private var _isAudioStart = false // audio capture start status
|
||||
val isReady: Boolean
|
||||
get() = _isReady
|
||||
val isStart: Boolean
|
||||
get() = _isStart
|
||||
val isAudioStart: Boolean
|
||||
get() = _isAudioStart
|
||||
}
|
||||
|
||||
private val logTag = "LOG_SERVICE"
|
||||
private val useVP9 = false
|
||||
private val binder = LocalBinder()
|
||||
|
||||
private var reuseVirtualDisplay = Build.VERSION.SDK_INT > 33
|
||||
|
||||
// video
|
||||
private var mediaProjection: MediaProjection? = null
|
||||
@@ -198,10 +221,7 @@ class MainService : Service() {
|
||||
private var virtualDisplay: VirtualDisplay? = null
|
||||
|
||||
// audio
|
||||
private var audioRecorder: AudioRecord? = null
|
||||
private var audioReader: AudioReader? = null
|
||||
private var minBufferSize = 0
|
||||
private var audioRecordStat = false
|
||||
private val audioRecordHandle = AudioRecordHandle(this, { isStart }, { isAudioStart })
|
||||
|
||||
// notification
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
@@ -210,8 +230,8 @@ class MainService : Service() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(logTag,"MainService onCreate")
|
||||
init(this)
|
||||
Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay")
|
||||
FFI.init(this)
|
||||
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
serviceLooper = looper
|
||||
@@ -223,16 +243,18 @@ class MainService : Service() {
|
||||
// keep the config dir same with flutter
|
||||
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
||||
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
||||
startServer(configPath)
|
||||
FFI.startServer(configPath, "")
|
||||
|
||||
createForegroundNotification()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
checkMediaPermission()
|
||||
stopService(Intent(this, FloatingWindowService::class.java))
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private var isHalfScale: Boolean? = null;
|
||||
private fun updateScreenInfo(orientation: Int) {
|
||||
var w: Int
|
||||
var h: Int
|
||||
@@ -265,7 +287,7 @@ class MainService : Service() {
|
||||
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||
var scale = 1
|
||||
if (w != 0 && h != 0) {
|
||||
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) {
|
||||
if (isHalfScale == true && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
||||
scale = 2
|
||||
w /= scale
|
||||
h /= scale
|
||||
@@ -278,8 +300,10 @@ class MainService : Service() {
|
||||
SCREEN_INFO.dpi = dpi
|
||||
if (isStart) {
|
||||
stopCapture()
|
||||
refreshScreen()
|
||||
FFI.refreshScreen()
|
||||
startCapture()
|
||||
} else {
|
||||
FFI.refreshScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +330,7 @@ class MainService : Service() {
|
||||
createForegroundNotification()
|
||||
|
||||
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
||||
startService()
|
||||
FFI.startService()
|
||||
}
|
||||
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
|
||||
val mediaProjectionManager =
|
||||
@@ -354,12 +378,13 @@ class MainService : Service() {
|
||||
).apply {
|
||||
setOnImageAvailableListener({ imageReader: ImageReader ->
|
||||
try {
|
||||
// If not call acquireLatestImage, listener will not be called again
|
||||
imageReader.acquireLatestImage().use { image ->
|
||||
if (image == null) return@setOnImageAvailableListener
|
||||
if (image == null || !isStart) return@setOnImageAvailableListener
|
||||
val planes = image.planes
|
||||
val buffer = planes[0].buffer
|
||||
buffer.rewind()
|
||||
onVideoFrameUpdate(buffer)
|
||||
FFI.onVideoFrameUpdate(buffer)
|
||||
}
|
||||
} catch (ignored: java.lang.Exception) {
|
||||
}
|
||||
@@ -370,6 +395,14 @@ class MainService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onVoiceCallStarted(): Boolean {
|
||||
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
||||
}
|
||||
|
||||
fun onVoiceCallClosed(): Boolean {
|
||||
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
||||
}
|
||||
|
||||
fun startCapture(): Boolean {
|
||||
if (isStart) {
|
||||
return true
|
||||
@@ -378,6 +411,7 @@ class MainService : Service() {
|
||||
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||
return false
|
||||
}
|
||||
|
||||
updateScreenInfo(resources.configuration.orientation)
|
||||
Log.d(logTag, "Start Capture")
|
||||
surface = createSurface()
|
||||
@@ -389,51 +423,71 @@ class MainService : Service() {
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
startAudioRecorder()
|
||||
if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
|
||||
Log.d(logTag, "createAudioRecorder fail")
|
||||
} else {
|
||||
Log.d(logTag, "audio recorder start")
|
||||
audioRecordHandle.startAudioRecorder()
|
||||
}
|
||||
}
|
||||
checkMediaPermission()
|
||||
_isStart = true
|
||||
setFrameRawEnable("video",true)
|
||||
setFrameRawEnable("audio",true)
|
||||
FFI.setFrameRawEnable("video",true)
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopCapture() {
|
||||
Log.d(logTag, "Stop Capture")
|
||||
setFrameRawEnable("video",false)
|
||||
setFrameRawEnable("audio",false)
|
||||
FFI.setFrameRawEnable("video",false)
|
||||
_isStart = false
|
||||
// release video
|
||||
virtualDisplay?.release()
|
||||
surface?.release()
|
||||
if (reuseVirtualDisplay) {
|
||||
// The virtual display video projection can be paused by calling `setSurface(null)`.
|
||||
// https://developer.android.com/reference/android/hardware/display/VirtualDisplay.Callback
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/android.hardware.display.virtualdisplay.callback.onpaused?view=net-android-34.0
|
||||
virtualDisplay?.setSurface(null)
|
||||
} else {
|
||||
virtualDisplay?.release()
|
||||
}
|
||||
// suface needs to be release after `imageReader.close()` to imageReader access released surface
|
||||
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
|
||||
imageReader?.close()
|
||||
imageReader = null
|
||||
videoEncoder?.let {
|
||||
it.signalEndOfInputStream()
|
||||
it.stop()
|
||||
it.release()
|
||||
}
|
||||
virtualDisplay = null
|
||||
if (!reuseVirtualDisplay) {
|
||||
virtualDisplay = null
|
||||
}
|
||||
videoEncoder = null
|
||||
// suface needs to be release after `imageReader.close()` to imageReader access released surface
|
||||
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
|
||||
surface?.release()
|
||||
|
||||
// release audio
|
||||
audioRecordStat = false
|
||||
audioRecorder?.release()
|
||||
audioRecorder = null
|
||||
minBufferSize = 0
|
||||
_isAudioStart = false
|
||||
audioRecordHandle.tryReleaseAudio()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
Log.d(logTag, "destroy service")
|
||||
_isReady = false
|
||||
_isAudioStart = false
|
||||
|
||||
stopCapture()
|
||||
imageReader?.close()
|
||||
imageReader = null
|
||||
|
||||
if (reuseVirtualDisplay) {
|
||||
virtualDisplay?.release()
|
||||
virtualDisplay = null
|
||||
}
|
||||
|
||||
mediaProjection = null
|
||||
checkMediaPermission()
|
||||
stopForeground(true)
|
||||
stopService(Intent(this, FloatingWindowService::class.java))
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
@@ -459,11 +513,7 @@ class MainService : Service() {
|
||||
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||
return
|
||||
}
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface, null, null
|
||||
)
|
||||
createOrSetVirtualDisplay(mp, surface!!)
|
||||
}
|
||||
|
||||
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
||||
@@ -475,11 +525,28 @@ class MainService : Service() {
|
||||
}
|
||||
it.setCallback(cb)
|
||||
it.start()
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface, null, null
|
||||
)
|
||||
createOrSetVirtualDisplay(mp, surface!!)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
||||
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
||||
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
|
||||
try {
|
||||
virtualDisplay?.let {
|
||||
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
||||
it.setSurface(s)
|
||||
} ?: let {
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
s, null, null
|
||||
)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
|
||||
// This initiates a prompt dialog for the user to confirm screen projection.
|
||||
requestMediaProjection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +574,6 @@ class MainService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun createMediaCodec() {
|
||||
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
||||
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
||||
@@ -527,76 +593,6 @@ class MainService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun startAudioRecorder() {
|
||||
checkAudioRecorder()
|
||||
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||
try {
|
||||
audioRecorder!!.startRecording()
|
||||
audioRecordStat = true
|
||||
thread {
|
||||
while (audioRecordStat) {
|
||||
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||
onAudioFrameUpdate(it)
|
||||
}
|
||||
}
|
||||
Log.d(logTag, "Exit audio thread")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(logTag, "startAudioRecorder fail:$e")
|
||||
}
|
||||
} else {
|
||||
Log.d(logTag, "startAudioRecorder fail")
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun checkAudioRecorder() {
|
||||
if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
|
||||
return
|
||||
}
|
||||
// read f32 to byte , length * 4
|
||||
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||
AUDIO_SAMPLE_RATE,
|
||||
AUDIO_CHANNEL_MASK,
|
||||
AUDIO_ENCODING
|
||||
)
|
||||
if (minBufferSize == 0) {
|
||||
Log.d(logTag, "get min buffer size fail!")
|
||||
return
|
||||
}
|
||||
audioReader = AudioReader(minBufferSize, 4)
|
||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
mediaProjection?.let {
|
||||
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
||||
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
audioRecorder = AudioRecord.Builder()
|
||||
.setAudioFormat(
|
||||
AudioFormat.Builder()
|
||||
.setEncoding(AUDIO_ENCODING)
|
||||
.setSampleRate(AUDIO_SAMPLE_RATE)
|
||||
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
||||
)
|
||||
.setAudioPlaybackCaptureConfig(apcc)
|
||||
.setBufferSizeInBytes(minBufferSize).build()
|
||||
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||
return
|
||||
}
|
||||
}
|
||||
Log.d(logTag, "createAudioRecorder fail")
|
||||
}
|
||||
|
||||
private fun initNotification() {
|
||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -681,6 +677,21 @@ class MainService : Service() {
|
||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||
}
|
||||
|
||||
private fun voiceCallRequestNotification(
|
||||
clientID: Int,
|
||||
type: String,
|
||||
username: String,
|
||||
peerId: String
|
||||
) {
|
||||
val notification = notificationBuilder
|
||||
.setOngoing(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setContentTitle(translate("Do you accept?"))
|
||||
.setContentText("$type:$username-$peerId")
|
||||
.build()
|
||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||
}
|
||||
|
||||
private fun getClientNotifyID(clientID: Int): Int {
|
||||
return clientID + NOTIFY_ID_OFFSET
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
|
||||
import hbb.MessageOuterClass.ClipboardFormat
|
||||
import hbb.MessageOuterClass.Clipboard
|
||||
import hbb.MessageOuterClass.MultiClipboards
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
class RdClipboardManager(private val clipboardManager: ClipboardManager) {
|
||||
private val logTag = "RdClipboardManager"
|
||||
private val supportedMimeTypes = arrayOf(
|
||||
ClipDescription.MIMETYPE_TEXT_PLAIN,
|
||||
ClipDescription.MIMETYPE_TEXT_HTML
|
||||
)
|
||||
|
||||
// 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`.
|
||||
// 2. Avoid sending the clipboard data before enabling client clipboard.
|
||||
// 1) Disable clipboard
|
||||
// 2) Copy text "a"
|
||||
// 3) Enable clipboard
|
||||
// 4) Switch to another app
|
||||
// 5) Switch back to the app
|
||||
// 6) "a" should not be sent to the client, because it's copied before enabling clipboard
|
||||
//
|
||||
// It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`,
|
||||
// though the `lastUpdatedClipData` will be set to null once.
|
||||
private var lastUpdatedClipData: ClipData? = null
|
||||
private var isClientEnabled = true;
|
||||
private var _isListening = false;
|
||||
val isListening: Boolean
|
||||
get() = _isListening
|
||||
|
||||
fun checkPrimaryClip(isClient: Boolean, isSync: Boolean) {
|
||||
val clipData = clipboardManager.primaryClip
|
||||
if (clipData != null && clipData.itemCount > 0) {
|
||||
// Only handle the first item in the clipboard for now.
|
||||
val clip = clipData.getItemAt(0)
|
||||
val isHostSync = !isClient && isSync
|
||||
// Ignore the `isClipboardDataEqual()` check if it's a host sync operation.
|
||||
// Because it's a action manually triggered by the user.
|
||||
if (!isHostSync) {
|
||||
if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) {
|
||||
Log.d(logTag, "Clipboard data is the same as last update, ignore")
|
||||
return
|
||||
}
|
||||
}
|
||||
val mimeTypeCount = clipData.description.getMimeTypeCount()
|
||||
val mimeTypes = mutableListOf<String>()
|
||||
for (i in 0 until mimeTypeCount) {
|
||||
mimeTypes.add(clipData.description.getMimeType(i))
|
||||
}
|
||||
var text: CharSequence? = null;
|
||||
var html: String? = null;
|
||||
if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
|
||||
text = clip?.text
|
||||
}
|
||||
if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
|
||||
text = clip?.text
|
||||
html = clip?.htmlText
|
||||
}
|
||||
var count = 0
|
||||
val clips = MultiClipboards.newBuilder()
|
||||
if (text != null) {
|
||||
val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString())
|
||||
clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build())
|
||||
count++
|
||||
}
|
||||
if (html != null) {
|
||||
val content = com.google.protobuf.ByteString.copyFromUtf8(html)
|
||||
clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build())
|
||||
count++
|
||||
}
|
||||
if (count > 0) {
|
||||
val clipsBytes = clips.build().toByteArray()
|
||||
val isClientFlag = if (isClient) 1 else 0
|
||||
val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply {
|
||||
put(isClientFlag.toByte())
|
||||
put(clipsBytes)
|
||||
}
|
||||
clipsBuf.flip()
|
||||
lastUpdatedClipData = clipData
|
||||
Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote")
|
||||
FFI.onClipboardUpdate(clipsBuf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val clipboardListener = object : ClipboardManager.OnPrimaryClipChangedListener {
|
||||
override fun onPrimaryClipChanged() {
|
||||
Log.d(logTag, "onPrimaryClipChanged")
|
||||
checkPrimaryClip(true, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSupportedMimeType(mimeType: String): Boolean {
|
||||
return supportedMimeTypes.contains(mimeType)
|
||||
}
|
||||
|
||||
private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean {
|
||||
if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) {
|
||||
return false
|
||||
}
|
||||
val mimeTypeCount = left.description.getMimeTypeCount()
|
||||
for (i in 0 until mimeTypeCount) {
|
||||
if (left.description.getMimeType(i) != right.description.getMimeType(i)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (left.itemCount != right.itemCount) {
|
||||
return false
|
||||
}
|
||||
for (i in 0 until left.itemCount) {
|
||||
val mimeType = left.description.getMimeType(i)
|
||||
if (!isSupportedMimeType(mimeType)) {
|
||||
continue
|
||||
}
|
||||
val leftItem = left.getItemAt(i)
|
||||
val rightItem = right.getItemAt(i)
|
||||
if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) {
|
||||
if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun rustEnableServiceClipboard(enable: Boolean) {
|
||||
Log.d(logTag, "rustEnableServiceClipboard: enable: $enable, _isListening: $_isListening")
|
||||
if (enable) {
|
||||
if (!_isListening) {
|
||||
clipboardManager.addPrimaryClipChangedListener(clipboardListener)
|
||||
_isListening = true
|
||||
}
|
||||
} else {
|
||||
if (_isListening) {
|
||||
clipboardManager.removePrimaryClipChangedListener(clipboardListener)
|
||||
_isListening = false
|
||||
lastUpdatedClipData = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun rustEnableClientClipboard(enable: Boolean) {
|
||||
Log.d(logTag, "rustEnableClientClipboard: enable: $enable")
|
||||
isClientEnabled = enable
|
||||
if (enable) {
|
||||
lastUpdatedClipData = clipboardManager.primaryClip
|
||||
} else {
|
||||
lastUpdatedClipData = null
|
||||
}
|
||||
}
|
||||
|
||||
fun syncClipboard(isClient: Boolean) {
|
||||
Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled, _isListening: $_isListening")
|
||||
if (isClient && !isClientEnabled) {
|
||||
return
|
||||
}
|
||||
if (!isClient && !_isListening) {
|
||||
return
|
||||
}
|
||||
checkPrimaryClip(isClient, true)
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun rustUpdateClipboard(clips: ByteArray) {
|
||||
val clips = MultiClipboards.parseFrom(clips)
|
||||
var mimeTypes = mutableListOf<String>()
|
||||
var text: String? = null
|
||||
var html: String? = null
|
||||
for (clip in clips.getClipboardsList()) {
|
||||
when (clip.format) {
|
||||
ClipboardFormat.Text -> {
|
||||
mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN)
|
||||
text = String(clip.content.toByteArray(), Charsets.UTF_8)
|
||||
}
|
||||
ClipboardFormat.Html -> {
|
||||
mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML)
|
||||
html = String(clip.content.toByteArray(), Charsets.UTF_8)
|
||||
}
|
||||
ClipboardFormat.ImageRgba -> {
|
||||
}
|
||||
ClipboardFormat.ImagePng -> {
|
||||
}
|
||||
else -> {
|
||||
Log.e(logTag, "Unsupported clipboard format: ${clip.format}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray())
|
||||
var item: ClipData.Item? = null
|
||||
if (text == null) {
|
||||
Log.e(logTag, "No text content in clipboard")
|
||||
return
|
||||
} else {
|
||||
if (html == null) {
|
||||
item = ClipData.Item(text)
|
||||
} else {
|
||||
item = ClipData.Item(text, html)
|
||||
}
|
||||
}
|
||||
if (item == null) {
|
||||
Log.e(logTag, "No item in clipboard")
|
||||
return
|
||||
}
|
||||
val clipData = ClipData(clipDescription, item)
|
||||
lastUpdatedClipData = clipData
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
// Inspired by https://github.com/yosemiteyss/flutter_volume_controller/blob/main/android/src/main/kotlin/com/yosemiteyss/flutter_volume_controller/VolumeController.kt
|
||||
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
|
||||
class VolumeController(private val audioManager: AudioManager) {
|
||||
private val logTag = "volume controller"
|
||||
|
||||
fun getVolume(streamType: Int): Double {
|
||||
val current = audioManager.getStreamVolume(streamType)
|
||||
val max = audioManager.getStreamMaxVolume(streamType)
|
||||
return current.toDouble() / max
|
||||
}
|
||||
|
||||
fun setVolume(volume: Double, showSystemUI: Boolean, streamType: Int) {
|
||||
val max = audioManager.getStreamMaxVolume(streamType)
|
||||
audioManager.setStreamVolume(
|
||||
streamType,
|
||||
(max * volume).toInt(),
|
||||
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||
)
|
||||
}
|
||||
|
||||
fun raiseVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
|
||||
if (step == null) {
|
||||
audioManager.adjustStreamVolume(
|
||||
streamType,
|
||||
AudioManager.ADJUST_RAISE,
|
||||
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||
)
|
||||
} else {
|
||||
val target = getVolume(streamType) + step
|
||||
setVolume(target, showSystemUI, streamType)
|
||||
}
|
||||
}
|
||||
|
||||
fun lowerVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
|
||||
if (step == null) {
|
||||
audioManager.adjustStreamVolume(
|
||||
streamType,
|
||||
AudioManager.ADJUST_LOWER,
|
||||
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||
)
|
||||
} else {
|
||||
val target = getVolume(streamType) - step
|
||||
setVolume(target, showSystemUI, streamType)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMute(streamType: Int): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
audioManager.isStreamMute(streamType)
|
||||
} else {
|
||||
audioManager.getStreamVolume(streamType) == 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMute(isMuted: Boolean, showSystemUI: Boolean, streamType: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
audioManager.adjustStreamVolume(
|
||||
streamType,
|
||||
if (isMuted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
|
||||
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||
)
|
||||
} else {
|
||||
audioManager.setStreamMute(streamType, isMuted)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleMute(showSystemUI: Boolean, streamType: Int) {
|
||||
val isMuted = getMute(streamType)
|
||||
setMute(!isMuted, showSystemUI, streamType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,14 @@ import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.*
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import com.hjq.permissions.Permission
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import ffi.FFI
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
@@ -43,6 +47,9 @@ const val START_ACTION = "start_action"
|
||||
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
||||
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
||||
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
||||
const val GET_VALUE = "get_value"
|
||||
|
||||
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
|
||||
|
||||
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
|
||||
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
|
||||
@@ -56,6 +63,11 @@ data class Info(
|
||||
var width: Int, var height: Int, var scale: Int, var dpi: Int
|
||||
)
|
||||
|
||||
fun isSupportVoiceCall(): Boolean {
|
||||
// https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
}
|
||||
|
||||
fun requestPermission(context: Context, type: String) {
|
||||
XXPermissions.with(context)
|
||||
.permission(type)
|
||||
@@ -120,3 +132,26 @@ class AudioReader(val bufSize: Int, private val maxFrames: Int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
|
||||
var w = 0
|
||||
var h = 0
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val m = windowManager.maximumWindowMetrics
|
||||
w = m.bounds.width()
|
||||
h = m.bounds.height()
|
||||
} else {
|
||||
val dm = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getRealMetrics(dm)
|
||||
w = dm.widthPixels
|
||||
h = dm.heightPixels
|
||||
}
|
||||
return Pair(w, h)
|
||||
}
|
||||
|
||||
fun translate(input: String): String {
|
||||
Log.d("common", "translate:$LOCAL_NAME")
|
||||
return FFI.translateLocale(LOCAL_NAME, input)
|
||||
}
|
||||
27
flutter/android/app/src/main/kotlin/ffi.kt
Normal file
27
flutter/android/app/src/main/kotlin/ffi.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
// ffi.kt
|
||||
|
||||
package ffi
|
||||
|
||||
import android.content.Context
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
import com.carriez.flutter_hbb.RdClipboardManager
|
||||
|
||||
object FFI {
|
||||
init {
|
||||
System.loadLibrary("rustdesk")
|
||||
}
|
||||
|
||||
external fun init(ctx: Context)
|
||||
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
||||
external fun startServer(app_dir: String, custom_client_config: String)
|
||||
external fun startService()
|
||||
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||
external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||
external fun translateLocale(localeName: String, input: String): String
|
||||
external fun refreshScreen()
|
||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
external fun setCodecInfo(info: String)
|
||||
external fun getLocalOption(key: String): String
|
||||
external fun onClipboardUpdate(clips: ByteBuffer)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:height="320dp" android:viewportHeight="32" android:viewportWidth="32" android:width="320dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M16,0L16,0A16,16 0,0 1,32 16L32,16A16,16 0,0 1,16 32L16,32A16,16 0,0 1,0 16L0,16A16,16 0,0 1,16 0z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
|
||||
<path android:fillColor="#1a1a1a" android:pathData="m23.89,10.135 l-1.807,1.795c-0.318,0.285 -0.472,0.744 -0.293,1.131 1.204,2.518 0.747,5.52 -1.228,7.494 -1.976,1.973 -4.981,2.429 -7.502,1.226 -0.371,-0.166 -0.807,-0.025 -1.093,0.265l-1.836,1.833c-0.216,0.211 -0.322,0.51 -0.288,0.809 0.034,0.3 0.206,0.567 0.463,0.723 4.326,2.618 9.882,1.951 13.463,-1.618 3.581,-3.568 4.264,-9.115 1.655,-13.443 -0.15,-0.263 -0.414,-0.442 -0.714,-0.484 -0.3,-0.043 -0.603,0.058 -0.819,0.269zM8.265,8.184c-3.599,3.554 -4.304,9.103 -1.709,13.441 0.15,0.264 0.413,0.443 0.714,0.485 0.3,0.042 0.603,-0.058 0.82,-0.27l1.797,-1.785c0.325,-0.285 0.484,-0.749 0.303,-1.141 -1.204,-2.518 -0.748,-5.52 1.228,-7.493 1.975,-1.973 4.981,-2.429 7.502,-1.227 0.367,0.165 0.797,0.028 1.084,-0.254l1.846,-1.844c0.216,-0.211 0.322,-0.509 0.288,-0.809 -0.035,-0.299 -0.206,-0.566 -0.463,-0.723 -4.334,-2.596 -9.881,-1.908 -13.448,1.668z" android:strokeWidth="0.987992"/>
|
||||
|
||||
</vector>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,18 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.google.gms:google-services:4.3.14'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
@@ -24,6 +9,8 @@ allprojects {
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
1
flutter/assets/message_24dp_5F6368.svg
Normal file
1
flutter/assets/message_24dp_5F6368.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="-4 -4 32 32" width="24px" fill="#5f6368"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 277 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="-186 -186 1365 1365"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -68,6 +68,7 @@ function build {
|
||||
pushd "$SCRIPTDIR/.."
|
||||
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
|
||||
popd
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
|
||||
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
|
||||
|
||||
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
|
||||
|
||||
617
flutter/build_fdroid.sh
Executable file
617
flutter/build_fdroid.sh
Executable file
@@ -0,0 +1,617 @@
|
||||
#!/bin/bash
|
||||
|
||||
#
|
||||
# Script to build F-Droid release of RustDesk
|
||||
#
|
||||
# Copyright (C) 2024, The RustDesk Authors
|
||||
# 2024, Vasyl Gello <vasek.gello@gmail.com>
|
||||
#
|
||||
|
||||
# The script is invoked by F-Droid builder system ste-by-step.
|
||||
#
|
||||
# It accepts the following arguments:
|
||||
#
|
||||
# - versionName from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
|
||||
# - versionCode from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
|
||||
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
|
||||
# - The build step to execute:
|
||||
#
|
||||
# + sudo-deps: as root, install needed Debian packages into builder VM
|
||||
# + prebuild: patch sources and do other stuff before the build
|
||||
# + build: perform actual build of APK file
|
||||
#
|
||||
|
||||
# Start of functions
|
||||
|
||||
# Install Flutter of version `VERSION` from Github repository
|
||||
# into directory `FLUTTER_DIR` and apply patches if needed
|
||||
|
||||
prepare_flutter() {
|
||||
VERSION="${1}"
|
||||
FLUTTER_DIR="${2}"
|
||||
|
||||
if [ ! -f "${FLUTTER_DIR}/bin/flutter" ]; then
|
||||
git clone https://github.com/flutter/flutter "${FLUTTER_DIR}"
|
||||
fi
|
||||
|
||||
pushd "${FLUTTER_DIR}"
|
||||
|
||||
git restore .
|
||||
git checkout "${VERSION}"
|
||||
|
||||
# Patch flutter
|
||||
|
||||
if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then
|
||||
git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff"
|
||||
fi
|
||||
|
||||
flutter config --no-analytics
|
||||
|
||||
popd # ${FLUTTER_DIR}
|
||||
}
|
||||
|
||||
# Start of script
|
||||
|
||||
set -x
|
||||
|
||||
# Note current working directory as root dir for patches
|
||||
|
||||
ROOTDIR="${PWD}"
|
||||
|
||||
# Parse command-line arguments
|
||||
|
||||
VERNAME="${1}"
|
||||
VERCODE="${2}"
|
||||
ANDROID_ABI="${3}"
|
||||
BUILDSTEP="${4}"
|
||||
|
||||
if [ -z "${VERNAME}" ] || [ -z "${VERCODE}" ] || [ -z "${ANDROID_ABI}" ] ||
|
||||
[ -z "${BUILDSTEP}" ]; then
|
||||
echo "ERROR: Command-line arguments are all required to be non-empty!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set various architecture-specific identifiers
|
||||
|
||||
case "${ANDROID_ABI}" in
|
||||
arm64-v8a)
|
||||
FLUTTER_TARGET=android-arm64
|
||||
NDK_TARGET=aarch64-linux-android
|
||||
RUST_TARGET=aarch64-linux-android
|
||||
RUSTDESK_FEATURES='flutter,hwcodec'
|
||||
;;
|
||||
armeabi-v7a)
|
||||
FLUTTER_TARGET=android-arm
|
||||
NDK_TARGET=arm-linux-androideabi
|
||||
RUST_TARGET=armv7-linux-androideabi
|
||||
RUSTDESK_FEATURES='flutter,hwcodec'
|
||||
;;
|
||||
x86_64)
|
||||
FLUTTER_TARGET=android-x64
|
||||
NDK_TARGET=x86_64-linux-android
|
||||
RUST_TARGET=x86_64-linux-android
|
||||
RUSTDESK_FEATURES='flutter'
|
||||
;;
|
||||
x86)
|
||||
FLUTTER_TARGET=android-x86
|
||||
NDK_TARGET=i686-linux-android
|
||||
RUST_TARGET=i686-linux-android
|
||||
RUSTDESK_FEATURES='flutter'
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown Android ABI '${ANDROID_ABI}'!" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check ANDROID_SDK_ROOT and sdkmanager present on PATH
|
||||
|
||||
if [ ! -d "${ANDROID_SDK_ROOT}" ] || ! command -v sdkmanager 1>/dev/null; then
|
||||
echo "ERROR: Can not find Android SDK!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Export necessary variables
|
||||
|
||||
export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
|
||||
|
||||
export VCPKG_ROOT="${HOME}/vcpkg"
|
||||
|
||||
# Now act depending on build step
|
||||
|
||||
# NOTE: F-Droid maintainers require explicit declaration of dependencies
|
||||
# as root via `Builds.sudo` F-Droid metadata directive:
|
||||
# https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695
|
||||
|
||||
case "${BUILDSTEP}" in
|
||||
prebuild)
|
||||
# prebuild: patch sources and do other stuff before the build
|
||||
|
||||
#
|
||||
# Extract required versions for NDK, Rust, Flutter from
|
||||
# '.github/workflows/flutter-build.yml'
|
||||
#
|
||||
|
||||
CARGO_NDK_VERSION="$(yq -r \
|
||||
.env.CARGO_NDK_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
# Flutter used to compile main Rustdesk library
|
||||
|
||||
FLUTTER_VERSION="$(yq -r \
|
||||
.env.ANDROID_FLUTTER_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||
FLUTTER_VERSION="$(yq -r \
|
||||
.env.FLUTTER_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
fi
|
||||
|
||||
# Flutter used to compile Flutter<->Rust bridge files
|
||||
|
||||
FLUTTER_BRIDGE_VERSION="$(yq -r \
|
||||
.env.FLUTTER_VERSION \
|
||||
.github/workflows/bridge.yml)"
|
||||
|
||||
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
|
||||
.env.FLUTTER_RUST_BRIDGE_VERSION \
|
||||
.github/workflows/bridge.yml)"
|
||||
|
||||
NDK_VERSION="$(yq -r \
|
||||
.env.NDK_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
RUST_VERSION="$(yq -r \
|
||||
.env.RUST_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
VCPKG_COMMIT_ID="$(yq -r \
|
||||
.env.VCPKG_COMMIT_ID \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
|
||||
[ -z "${FLUTTER_BRIDGE_VERSION}" ] ||
|
||||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
|
||||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
|
||||
[ -z "${VCPKG_COMMIT_ID}" ]; then
|
||||
echo "ERROR: Can not identify all required versions!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Map NDK version to revision
|
||||
|
||||
NDK_VERSION="$(wget \
|
||||
-qO- \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
'https://api.github.com/repos/android/ndk/releases' |
|
||||
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||
|
||||
if [ -z "${NDK_VERSION}" ]; then
|
||||
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||
|
||||
#
|
||||
# Install the components
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Install Android NDK
|
||||
|
||||
if [ ! -d "${ANDROID_NDK_ROOT}" ]; then
|
||||
sdkmanager --install "ndk;${NDK_VERSION}"
|
||||
fi
|
||||
|
||||
# Install Rust
|
||||
|
||||
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
|
||||
pushd "${HOME}"
|
||||
|
||||
git clone --depth 1 https://github.com/rust-lang/rustup
|
||||
|
||||
popd # ${HOME}
|
||||
fi
|
||||
|
||||
pushd "${HOME}/rustup"
|
||||
bash rustup-init.sh -y \
|
||||
--target "${RUST_TARGET}" \
|
||||
--default-toolchain "${RUST_VERSION}"
|
||||
popd
|
||||
|
||||
if ! command -v cargo 1>/dev/null 2>&1; then
|
||||
. "${HOME}/.cargo/env"
|
||||
fi
|
||||
|
||||
# Install cargo-ndk
|
||||
|
||||
cargo install \
|
||||
cargo-ndk \
|
||||
--version "${CARGO_NDK_VERSION}" \
|
||||
--locked
|
||||
|
||||
# Install rust bridge generator
|
||||
|
||||
cargo install cargo-expand
|
||||
cargo install flutter_rust_bridge_codegen \
|
||||
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
|
||||
--features "uuid" \
|
||||
--locked
|
||||
|
||||
# Populate native vcpkg dependencies
|
||||
|
||||
if [ ! -d "${VCPKG_ROOT}" ]; then
|
||||
pushd "${HOME}"
|
||||
|
||||
git clone \
|
||||
https://github.com/Microsoft/vcpkg.git
|
||||
git clone \
|
||||
https://github.com/Microsoft/vcpkg-tool.git
|
||||
|
||||
pushd vcpkg-tool
|
||||
|
||||
mkdir build
|
||||
|
||||
pushd build
|
||||
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-G 'Ninja' \
|
||||
-DVCPKG_DEVELOPMENT_WARNINGS=OFF \
|
||||
..
|
||||
|
||||
cmake --build .
|
||||
|
||||
popd # build
|
||||
|
||||
popd # vcpkg-tool
|
||||
|
||||
pushd vcpkg
|
||||
|
||||
git reset --hard "${VCPKG_COMMIT_ID}"
|
||||
|
||||
cp -a ../vcpkg-tool/build/vcpkg vcpkg
|
||||
|
||||
# disable telemetry
|
||||
|
||||
touch "vcpkg.disable-metrics"
|
||||
|
||||
popd # vcpkg
|
||||
|
||||
popd # ${HOME}
|
||||
fi
|
||||
|
||||
# Install depot-tools for x86
|
||||
|
||||
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||
if [ ! -d "${HOME}/depot_tools" ]; then
|
||||
pushd "${HOME}"
|
||||
|
||||
git clone \
|
||||
--depth 1 \
|
||||
https://chromium.googlesource.com/chromium/tools/depot_tools.git
|
||||
|
||||
popd # ${HOME}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patch the RustDesk sources
|
||||
|
||||
git apply res/fdroid/patches/*.patch
|
||||
|
||||
# If Flutter version used to generate bridge files differs from Flutter
|
||||
# version used to compile Rustdesk library, generate bridge using the
|
||||
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
|
||||
|
||||
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
|
||||
# Install Flutter bridge version
|
||||
|
||||
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
|
||||
|
||||
# Save changes
|
||||
|
||||
git add .
|
||||
|
||||
# Edit pubspec to make flutter bridge version work
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \
|
||||
flutter/pubspec.yaml
|
||||
|
||||
# Download Flutter dependencies
|
||||
|
||||
pushd flutter
|
||||
|
||||
flutter clean
|
||||
flutter packages pub get
|
||||
|
||||
popd # flutter
|
||||
|
||||
# Generate FFI bindings
|
||||
|
||||
flutter_rust_bridge_codegen \
|
||||
--rust-input ./src/flutter_ffi.rs \
|
||||
--dart-output ./flutter/lib/generated_bridge.dart
|
||||
|
||||
# Add bridge files to save-list
|
||||
|
||||
git add -f ./flutter/lib/generated_bridge.* ./src/bridge_generated.*
|
||||
|
||||
# Restore everything
|
||||
|
||||
git checkout '*'
|
||||
git clean -dffx
|
||||
git reset
|
||||
fi
|
||||
|
||||
# Install Flutter version for RustDesk library build
|
||||
|
||||
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
|
||||
|
||||
# gms is not in thoes files now, but we still keep the following line for future reference(maybe).
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e '/gms/d' \
|
||||
flutter/android/build.gradle \
|
||||
flutter/android/app/build.gradle
|
||||
|
||||
# `firebase_analytics` is not in these files now, but we still keep the following lines.
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e '/firebase_analytics/d' \
|
||||
flutter/pubspec.yaml
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e '/ firebase/,/ version/d' \
|
||||
flutter/pubspec.lock
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e '/firebase/Id' \
|
||||
flutter/lib/main.dart
|
||||
|
||||
;;
|
||||
build)
|
||||
# build: perform actual build of APK file
|
||||
|
||||
set -e
|
||||
|
||||
#
|
||||
# Extract required versions for NDK, Rust, Flutter from
|
||||
# '.github/workflows/flutter-build.yml'
|
||||
#
|
||||
|
||||
# Flutter used to compile main Rustdesk library
|
||||
|
||||
FLUTTER_VERSION="$(yq -r \
|
||||
.env.ANDROID_FLUTTER_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||
FLUTTER_VERSION="$(yq -r \
|
||||
.env.FLUTTER_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
fi
|
||||
|
||||
NDK_VERSION="$(yq -r \
|
||||
.env.NDK_VERSION \
|
||||
.github/workflows/flutter-build.yml)"
|
||||
|
||||
# Map NDK version to revision
|
||||
|
||||
NDK_VERSION="$(wget \
|
||||
-qO- \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
'https://api.github.com/repos/android/ndk/releases' |
|
||||
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||
|
||||
if [ -z "${NDK_VERSION}" ]; then
|
||||
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||
|
||||
if ! command -v cargo 1>/dev/null 2>&1; then
|
||||
. "${HOME}/.cargo/env"
|
||||
fi
|
||||
|
||||
# Download Flutter dependencies
|
||||
|
||||
pushd flutter
|
||||
|
||||
flutter clean
|
||||
flutter packages pub get
|
||||
|
||||
popd # flutter
|
||||
|
||||
# Build host android deps
|
||||
|
||||
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
|
||||
|
||||
# Build rustdesk lib
|
||||
|
||||
cargo ndk \
|
||||
--platform 21 \
|
||||
--target "${RUST_TARGET}" \
|
||||
--bindgen \
|
||||
build \
|
||||
--release \
|
||||
--features "${RUSTDESK_FEATURES}"
|
||||
|
||||
mkdir -p "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"
|
||||
|
||||
cp "target/${RUST_TARGET}/release/liblibrustdesk.so" \
|
||||
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so"
|
||||
|
||||
cp "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/${NDK_TARGET}/libc++_shared.so" \
|
||||
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/"
|
||||
|
||||
"${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" \
|
||||
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"/*
|
||||
|
||||
# Build flutter-jit-release for x86
|
||||
|
||||
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||
pushd flutter-sdk
|
||||
|
||||
echo "## Sync flutter engine sources"
|
||||
echo "### We need fakeroot because chromium base image is unpacked with weird uid/gid ownership"
|
||||
|
||||
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" .gclient
|
||||
|
||||
export FAKEROOTDONTTRYCHOWN=1
|
||||
|
||||
fakeroot gclient sync
|
||||
|
||||
unset FAKEROOTDONTTRYCHOWN
|
||||
|
||||
pushd src
|
||||
|
||||
echo "## Patch away Google Play dependencies"
|
||||
|
||||
rm \
|
||||
flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java \
|
||||
flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e '/PlayStore/d' \
|
||||
flutter/tools/android_lint/project.xml \
|
||||
flutter/shell/platform/android/BUILD.gn
|
||||
|
||||
sed \
|
||||
-i \
|
||||
-e '/com.google.android.play/d' \
|
||||
flutter/tools/androidx/files.json
|
||||
|
||||
echo "## Configure android engine build"
|
||||
|
||||
flutter/tools/gn \
|
||||
--android --android-cpu x86 --runtime-mode=jit_release \
|
||||
--no-goma --no-enable-unittests
|
||||
|
||||
echo "## Perform android engine build"
|
||||
|
||||
ninja -C out/android_jit_release_x86
|
||||
|
||||
echo "## Configure host engine build"
|
||||
|
||||
flutter/tools/gn \
|
||||
--android-cpu x86 --runtime-mode=jit_release \
|
||||
--no-goma --no-enable-unittests
|
||||
|
||||
echo "## Perform android engine build"
|
||||
|
||||
ninja -C out/host_jit_release_x86
|
||||
|
||||
echo "## Rename host engine"
|
||||
|
||||
mv out/host_jit_release_x86 out/host_jit_release
|
||||
|
||||
echo "## Mimic jit_release engine to debug to use with flutter build apk"
|
||||
|
||||
pushd out/android_jit_release_x86
|
||||
|
||||
sed \
|
||||
-e 's/jit_release/debug/' \
|
||||
flutter_embedding_jit_release.maven-metadata.xml \
|
||||
1>flutter_embedding_debug.maven-metadata.xml
|
||||
|
||||
sed \
|
||||
-e 's/jit_release/debug/' \
|
||||
flutter_embedding_jit_release.pom \
|
||||
1>flutter_embedding_debug.pom
|
||||
|
||||
sed \
|
||||
-e 's/jit_release/debug/' \
|
||||
x86_jit_release.maven-metadata.xml \
|
||||
1>x86_debug.maven-metadata.xml
|
||||
|
||||
sed \
|
||||
-e 's/jit_release/debug/' \
|
||||
x86_jit_release.pom \
|
||||
1>x86_debug.pom
|
||||
|
||||
cp -a \
|
||||
flutter_embedding_jit_release-sources.jar \
|
||||
flutter_embedding_debug-sources.jar
|
||||
|
||||
cp -a \
|
||||
flutter_embedding_jit_release.jar \
|
||||
flutter_embedding_debug.jar
|
||||
|
||||
cp -a \
|
||||
x86_jit_release.jar \
|
||||
x86_debug.jar
|
||||
|
||||
popd # out/android_jit_release_x86
|
||||
|
||||
popd # src
|
||||
|
||||
popd # flutter-sdk
|
||||
|
||||
echo "# Clean up intermediate engine files and show free space"
|
||||
|
||||
rm -rf \
|
||||
flutter-sdk/src/out/android_jit_release_x86/obj \
|
||||
flutter-sdk/src/out/host_jit_release/obj
|
||||
|
||||
mv flutter-sdk/src/out flutter-out
|
||||
|
||||
rm -rf flutter-sdk
|
||||
|
||||
mkdir -p flutter-sdk/src/
|
||||
|
||||
mv flutter-out flutter-sdk/src/out
|
||||
fi
|
||||
|
||||
# Build the apk
|
||||
|
||||
pushd flutter
|
||||
|
||||
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||
flutter build apk \
|
||||
--local-engine-src-path="$(readlink -mf "../flutter-sdk/src")" \
|
||||
--local-engine=android_jit_release_x86 \
|
||||
--debug \
|
||||
--build-number="${VERCODE}" \
|
||||
--build-name="${VERNAME}" \
|
||||
--target-platform "${FLUTTER_TARGET}"
|
||||
else
|
||||
flutter build apk \
|
||||
--release \
|
||||
--build-number="${VERCODE}" \
|
||||
--build-name="${VERNAME}" \
|
||||
--target-platform "${FLUTTER_TARGET}"
|
||||
fi
|
||||
|
||||
popd # flutter
|
||||
|
||||
rm -rf flutter-sdk
|
||||
|
||||
# Special step for fdroiddata CI builds to remove .gitconfig
|
||||
|
||||
rm -f /home/vagrant/.gitconfig
|
||||
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown build step '${BUILDSTEP}'!" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Report success
|
||||
|
||||
echo "All done!"
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
platform :ios, '12.0'
|
||||
platform :ios, '13.0'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
|
||||
@@ -137,6 +137,6 @@ SPEC CHECKSUMS:
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
|
||||
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
|
||||
PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
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 = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -491,7 +491,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -541,7 +541,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
cargo build --features flutter --release --target aarch64-apple-ios --lib
|
||||
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,16 +10,16 @@ class PrivacyModeState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||
final RxString state = ''.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxString>(state, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxString>(tag: key)) {
|
||||
Get.delete<RxString>(tag: key);
|
||||
} else {
|
||||
Get.find<RxString>(tag: key).value = '';
|
||||
}
|
||||
@@ -33,9 +33,9 @@ class BlockInputState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
@@ -43,8 +43,8 @@ class BlockInputState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ class CurrentDisplayState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||
final RxInt state = RxInt(0);
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxInt>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxInt>(tag: key).value = 0;
|
||||
}
|
||||
@@ -66,8 +66,8 @@ class CurrentDisplayState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||
Get.delete<RxInt>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,16 +105,16 @@ class ConnectionTypeState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<ConnectionType>(tag: key)) {
|
||||
final ConnectionType collectionType = ConnectionType();
|
||||
Get.put(collectionType, tag: key);
|
||||
Get.put<ConnectionType>(collectionType, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<ConnectionType>(tag: key)) {
|
||||
Get.delete<ConnectionType>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,9 +127,9 @@ class FingerprintState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||
final RxString state = ''.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxString>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxString>(tag: key).value = '';
|
||||
}
|
||||
@@ -137,8 +137,8 @@ class FingerprintState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxString>(tag: key)) {
|
||||
Get.delete<RxString>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,9 +150,9 @@ class ShowRemoteCursorState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
@@ -160,8 +160,31 @@ class ShowRemoteCursorState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
class ShowRemoteCursorLockState {
|
||||
static String tag(String id) => 'show_remote_cursor_lock_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,10 +196,10 @@ class KeyboardEnabledState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
// Server side, default true
|
||||
final RxBool state = true.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = true;
|
||||
}
|
||||
@@ -184,8 +207,8 @@ class KeyboardEnabledState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +220,9 @@ class RemoteCursorMovedState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
@@ -207,8 +230,8 @@ class RemoteCursorMovedState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,9 +243,9 @@ class RemoteCountState {
|
||||
|
||||
static void init() {
|
||||
final key = tag();
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||
final RxInt state = 1.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxInt>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxInt>(tag: key).value = 1;
|
||||
}
|
||||
@@ -230,8 +253,8 @@ class RemoteCountState {
|
||||
|
||||
static void delete() {
|
||||
final key = tag();
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||
Get.delete<RxInt>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +266,9 @@ class PeerBoolOption {
|
||||
|
||||
static void init(String id, String opt, bool Function() init_getter) {
|
||||
final key = tag(id, opt);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool value = RxBool(init_getter());
|
||||
Get.put(value, tag: key);
|
||||
Get.put<RxBool>(value, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = init_getter();
|
||||
}
|
||||
@@ -253,8 +276,8 @@ class PeerBoolOption {
|
||||
|
||||
static void delete(String id, String opt) {
|
||||
final key = tag(id, opt);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,9 +290,9 @@ class PeerStringOption {
|
||||
|
||||
static void init(String id, String opt, String Function() init_getter) {
|
||||
final key = tag(id, opt);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||
final RxString value = RxString(init_getter());
|
||||
Get.put(value, tag: key);
|
||||
Get.put<RxString>(value, tag: key);
|
||||
} else {
|
||||
Get.find<RxString>(tag: key).value = init_getter();
|
||||
}
|
||||
@@ -277,8 +300,8 @@ class PeerStringOption {
|
||||
|
||||
static void delete(String id, String opt) {
|
||||
final key = tag(id, opt);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxString>(tag: key)) {
|
||||
Get.delete<RxString>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,9 +314,9 @@ class UnreadChatCountState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||
final RxInt state = RxInt(0);
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxInt>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxInt>(tag: key).value = 0;
|
||||
}
|
||||
@@ -301,8 +324,8 @@ class UnreadChatCountState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||
Get.delete<RxInt>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,9 +338,10 @@ initSharedStates(String id) {
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
ShowRemoteCursorLockState.init(id);
|
||||
RemoteCursorMovedState.init(id);
|
||||
FingerprintState.init(id);
|
||||
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
||||
PeerBoolOption.init(id, kOptionZoomCursor, () => false);
|
||||
UnreadChatCountState.init(id);
|
||||
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
||||
}
|
||||
@@ -327,10 +351,11 @@ removeSharedStates(String id) {
|
||||
BlockInputState.delete(id);
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
ShowRemoteCursorLockState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
RemoteCursorMovedState.delete(id);
|
||||
FingerprintState.delete(id);
|
||||
PeerBoolOption.delete(id, 'zoom-cursor');
|
||||
PeerBoolOption.delete(id, kOptionZoomCursor);
|
||||
UnreadChatCountState.delete(id);
|
||||
if (isMobile) ConnectionTypeState.delete(id);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,9 +8,11 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
import 'package:get/get.dart';
|
||||
@@ -34,17 +37,14 @@ class AddressBook extends StatefulWidget {
|
||||
class _AddressBookState extends State<AddressBook> {
|
||||
var menuPos = RelativeRect.fill;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
if (!gFFI.userModel.isLogin) {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||
return netWorkErrorWidget();
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -63,15 +63,16 @@ class _AddressBookState extends State<AddressBook> {
|
||||
retry: null, // remove retry
|
||||
close: () => gFFI.abModel.currentAbPushError.value = ''),
|
||||
Expanded(
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? _buildAddressBookDesktop()
|
||||
: _buildAddressBookMobile())
|
||||
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? _buildAddressBookPortrait()
|
||||
: _buildAddressBookLandscape()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Widget _buildAddressBookDesktop() {
|
||||
Widget _buildAddressBookLandscape() {
|
||||
return Row(
|
||||
children: [
|
||||
Offstage(
|
||||
@@ -108,7 +109,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddressBookMobile() {
|
||||
Widget _buildAddressBookPortrait() {
|
||||
const padding = 8.0;
|
||||
return Column(
|
||||
children: [
|
||||
Offstage(
|
||||
@@ -119,7 +121,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.background)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(padding, 0, padding, padding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -129,7 +132,6 @@ class _AddressBookState extends State<AddressBook> {
|
||||
width: double.infinity,
|
||||
child: _buildTags(),
|
||||
),
|
||||
_buildAbPermission(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -189,42 +191,74 @@ class _AddressBookState extends State<AddressBook> {
|
||||
if (!names.contains(gFFI.abModel.currentName.value)) {
|
||||
return Offstage();
|
||||
}
|
||||
// order: personal, divider, character order
|
||||
// https://pub.dev/packages/dropdown_button2#3-dropdownbutton2-with-items-of-different-heights-like-dividers
|
||||
final personalAddressBookName = gFFI.abModel.personalAddressBookName();
|
||||
bool contains = names.remove(personalAddressBookName);
|
||||
names.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
|
||||
if (contains) {
|
||||
names.insert(0, personalAddressBookName);
|
||||
}
|
||||
|
||||
Row buildItem(String e, {bool button = false}) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: gFFI.abModel.translatedName(e),
|
||||
child: Text(
|
||||
gFFI.abModel.translatedName(e),
|
||||
style: button ? null : TextStyle(fontSize: 14.0),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: button ? TextAlign.center : null,
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final items = names
|
||||
.map((e) => DropdownMenuItem(value: e, child: buildItem(e)))
|
||||
.toList();
|
||||
var menuItemStyleData = MenuItemStyleData(height: 36);
|
||||
if (contains && items.length > 1) {
|
||||
items.insert(1, DropdownMenuItem(enabled: false, child: Divider()));
|
||||
List<double> customHeights = List.filled(items.length, 36);
|
||||
customHeights[1] = 4;
|
||||
menuItemStyleData = MenuItemStyleData(customHeights: customHeights);
|
||||
}
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
|
||||
final isOptFixed = isOptionFixed(kOptionCurrentAbName);
|
||||
return DropdownButton2<String>(
|
||||
value: gFFI.abModel.currentName.value,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
gFFI.abModel.setCurrentName(value);
|
||||
bind.setLocalFlutterOption(k: 'current-ab-name', v: value);
|
||||
}
|
||||
},
|
||||
onChanged: isOptFixed
|
||||
? null
|
||||
: (value) {
|
||||
if (value != null) {
|
||||
gFFI.abModel.setCurrentName(value);
|
||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||
}
|
||||
},
|
||||
customButton: Obx(() => Container(
|
||||
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child:
|
||||
buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||
Icon(Icons.arrow_drop_down),
|
||||
]),
|
||||
)),
|
||||
underline: Container(
|
||||
height: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
buttonStyleData: ButtonStyleData(height: 48),
|
||||
menuItemStyleData: MenuItemStyleData(height: 36),
|
||||
items: names
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: gFFI.abModel.translatedName(e),
|
||||
child: Text(
|
||||
gFFI.abModel.translatedName(e),
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
),
|
||||
],
|
||||
)))
|
||||
.toList(),
|
||||
menuItemStyleData: menuItemStyleData,
|
||||
items: items,
|
||||
isExpanded: true,
|
||||
isDense: true,
|
||||
dropdownSearchData: DropdownSearchData(
|
||||
searchController: textEditingController,
|
||||
searchInnerWidgetHeight: 50,
|
||||
@@ -283,13 +317,14 @@ class _AddressBookState extends State<AddressBook> {
|
||||
|
||||
Widget _buildTags() {
|
||||
return Obx(() {
|
||||
final List tags;
|
||||
List tags;
|
||||
if (gFFI.abModel.sortTags.value) {
|
||||
tags = gFFI.abModel.currentAbTags.toList();
|
||||
tags.sort();
|
||||
} else {
|
||||
tags = gFFI.abModel.currentAbTags;
|
||||
tags = gFFI.abModel.currentAbTags.toList();
|
||||
}
|
||||
tags = [kUntagged, ...tags].toList();
|
||||
final editPermission = gFFI.abModel.current.canWrite();
|
||||
tagBuilder(String e) {
|
||||
return AddressBookTag(
|
||||
@@ -305,8 +340,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
showActionMenu: editPermission);
|
||||
}
|
||||
|
||||
final gridView = DynamicGridView.builder(
|
||||
shrinkWrap: isMobile,
|
||||
gridView(bool isPortrait) => DynamicGridView.builder(
|
||||
shrinkWrap: isPortrait,
|
||||
gridDelegate: SliverGridDelegateWithWrapping(),
|
||||
itemCount: tags.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
@@ -314,9 +349,9 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return tagBuilder(e);
|
||||
});
|
||||
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return (isDesktop || isWebDesktop)
|
||||
? gridView
|
||||
: LimitedBox(maxHeight: maxHeight, child: gridView);
|
||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||
? gridView(false)
|
||||
: LimitedBox(maxHeight: maxHeight, child: gridView(true)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,13 +361,13 @@ class _AddressBookState extends State<AddressBook> {
|
||||
alignment: Alignment.topLeft,
|
||||
child: AddressBookPeersView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.abModel.currentAbPeers,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> syncMenuItem() {
|
||||
final isOptFixed = isOptionFixed(syncAbOption);
|
||||
return MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Sync with recent sessions'),
|
||||
@@ -343,11 +378,13 @@ class _AddressBookState extends State<AddressBook> {
|
||||
gFFI.abModel.setShouldAsync(v);
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
enabled: (!isOptFixed).obs,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> sortMenuItem() {
|
||||
final isOptFixed = isOptionFixed(sortAbTagsOption);
|
||||
return MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Sort tags'),
|
||||
@@ -355,15 +392,18 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return shouldSortTags();
|
||||
},
|
||||
setter: (bool v) async {
|
||||
bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : '');
|
||||
bind.mainSetLocalOption(
|
||||
key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo);
|
||||
gFFI.abModel.sortTags.value = v;
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
enabled: (!isOptFixed).obs,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> filterMenuItem() {
|
||||
final isOptFixed = isOptionFixed(filterAbTagOption);
|
||||
return MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Filter by intersection'),
|
||||
@@ -371,10 +411,12 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return filterAbTagByIntersection();
|
||||
},
|
||||
setter: (bool v) async {
|
||||
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
|
||||
bind.mainSetLocalOption(
|
||||
key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo);
|
||||
gFFI.abModel.filterByIntersection.value = v;
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
enabled: (!isOptFixed).obs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -384,11 +426,13 @@ class _AddressBookState extends State<AddressBook> {
|
||||
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
|
||||
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||
sortMenuItem(),
|
||||
syncMenuItem(),
|
||||
if (gFFI.abModel.legacyMode.value)
|
||||
sortMenuItem(), // It's already sorted after pulling down
|
||||
if (canWrite) syncMenuItem(),
|
||||
filterMenuItem(),
|
||||
if (!gFFI.abModel.legacyMode.value) MenuEntryDivider<String>(),
|
||||
if (!gFFI.abModel.legacyMode.value)
|
||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||
MenuEntryDivider<String>(),
|
||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||
getEntry(translate("ab_web_console_tip"), () async {
|
||||
final url = await bind.mainGetApiServer();
|
||||
if (await canLaunchUrlString(url)) {
|
||||
@@ -466,20 +510,21 @@ class _AddressBookState extends State<AddressBook> {
|
||||
double marginBottom = 4;
|
||||
|
||||
row({required Widget lable, required Widget input}) {
|
||||
return Row(
|
||||
children: [
|
||||
!isMobile
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 200),
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isMobile ? 8 : 0);
|
||||
makeChild(bool isPortrait) => Row(
|
||||
children: [
|
||||
!isPortrait
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 200),
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
@@ -502,23 +547,28 @@ class _AddressBookState extends State<AddressBook> {
|
||||
),
|
||||
],
|
||||
),
|
||||
input: TextField(
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
)),
|
||||
input: Obx(() => TextField(
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
))),
|
||||
row(
|
||||
lable: Text(
|
||||
translate('Alias'),
|
||||
style: style,
|
||||
),
|
||||
input: TextField(
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('Alias'),
|
||||
input: Obx(() => TextField(
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('Alias'),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (isCurrentAbShared)
|
||||
@@ -527,22 +577,26 @@ class _AddressBookState extends State<AddressBook> {
|
||||
translate('Password'),
|
||||
style: style,
|
||||
),
|
||||
input: TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
},
|
||||
input: Obx(
|
||||
() => TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: stateGlobal.isPortrait.isFalse
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
@@ -617,6 +671,14 @@ class _AddressBookState extends State<AddressBook> {
|
||||
} else {
|
||||
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = tags.join(',');
|
||||
for (var t in [kUntagged, translate(kUntagged)]) {
|
||||
if (tags.contains(t)) {
|
||||
BotToast.showText(
|
||||
contentColor: Colors.red, text: 'Tag name cannot be "$t"');
|
||||
isInProgress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
gFFI.abModel.addTags(tags);
|
||||
// final currentPeers
|
||||
}
|
||||
@@ -689,12 +751,14 @@ class AddressBookTag extends StatelessWidget {
|
||||
}
|
||||
|
||||
const double radius = 8;
|
||||
final isUnTagged = name == kUntagged;
|
||||
final showAction = showActionMenu && !isUnTagged;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
onTapDown: showActionMenu ? setPosition : null,
|
||||
onSecondaryTapDown: showActionMenu ? setPosition : null,
|
||||
onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null,
|
||||
onLongPress: showActionMenu ? () => _showMenu(context, pos) : null,
|
||||
onTapDown: showAction ? setPosition : null,
|
||||
onSecondaryTapDown: showAction ? setPosition : null,
|
||||
onSecondaryTap: showAction ? () => _showMenu(context, pos) : null,
|
||||
onLongPress: showAction ? () => _showMenu(context, pos) : null,
|
||||
child: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: tags.contains(name)
|
||||
@@ -706,17 +770,18 @@ class AddressBookTag extends StatelessWidget {
|
||||
child: IntrinsicWidth(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: tags.contains(name)
|
||||
? Colors.white
|
||||
: gFFI.abModel.getCurrentAbTagColor(name)),
|
||||
).marginOnly(right: radius / 2),
|
||||
if (!isUnTagged)
|
||||
Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: tags.contains(name)
|
||||
? Colors.white
|
||||
: gFFI.abModel.getCurrentAbTagColor(name)),
|
||||
).marginOnly(right: radius / 2),
|
||||
Expanded(
|
||||
child: Text(name,
|
||||
child: Text(isUnTagged ? translate(name) : name,
|
||||
style: TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: tags.contains(name) ? Colors.white : null)),
|
||||
|
||||
81
flutter/lib/common/widgets/audio_input.dart
Normal file
81
flutter/lib/common/widgets/audio_input.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
const _kSystemSound = 'System Sound';
|
||||
|
||||
typedef AudioINputSetDevice = void Function(String device);
|
||||
typedef AudioInputBuilder = Widget Function(
|
||||
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
||||
|
||||
class AudioInput extends StatelessWidget {
|
||||
final AudioInputBuilder builder;
|
||||
final bool isCm;
|
||||
final bool isVoiceCall;
|
||||
|
||||
const AudioInput(
|
||||
{Key? key,
|
||||
required this.builder,
|
||||
required this.isCm,
|
||||
required this.isVoiceCall})
|
||||
: super(key: key);
|
||||
|
||||
static String getDefault() {
|
||||
if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound);
|
||||
return '';
|
||||
}
|
||||
|
||||
static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
|
||||
if (isVoiceCall) {
|
||||
return bind.getVoiceCallInputDevice(isCm: isCm);
|
||||
} else {
|
||||
return bind.mainGetOption(key: 'audio-input');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
|
||||
String device = await getAudioInput(isCm, isVoiceCall);
|
||||
if (device.isNotEmpty) {
|
||||
return device;
|
||||
} else {
|
||||
return getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setDevice(
|
||||
String device, bool isCm, bool isVoiceCall) async {
|
||||
if (device == getDefault()) device = '';
|
||||
if (isVoiceCall) {
|
||||
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
|
||||
} else {
|
||||
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, Object>> getDevicesInfo(
|
||||
bool isCm, bool isVoiceCall) async {
|
||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||
if (bind.mainAudioSupportLoopback()) {
|
||||
devices.insert(0, translate(_kSystemSound));
|
||||
}
|
||||
String current = await getValue(isCm, isVoiceCall);
|
||||
return {'devices': devices, 'current': current};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return futureBuilder(
|
||||
future: getDevicesInfo(isCm, isVoiceCall),
|
||||
hasData: (data) {
|
||||
String currentDevice = data['current'];
|
||||
List<String> devices = data['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
return const Offstage();
|
||||
}
|
||||
return builder(devices, currentDevice, (devices) {
|
||||
setDevice(devices, isCm, isVoiceCall);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
message: !(isDesktop || isWebDesktop)
|
||||
? ''
|
||||
: widget.peer.tags.isNotEmpty
|
||||
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
|
||||
|
||||
38
flutter/lib/common/widgets/connection_page_title.dart
Normal file
38
flutter/lib/common/widgets/connection_page_title.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
|
||||
Widget getConnectionPageTitle(BuildContext context, bool isWeb) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
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: 300),
|
||||
message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
|
||||
String get name => translate('uppercase');
|
||||
@override
|
||||
bool validate(String value) {
|
||||
return value.contains(RegExp(r'[A-Z]'));
|
||||
return value.runes.any((int rune) {
|
||||
var character = String.fromCharCode(rune);
|
||||
return character.toUpperCase() == character &&
|
||||
character.toLowerCase() != character;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
|
||||
|
||||
@override
|
||||
bool validate(String value) {
|
||||
return value.contains(RegExp(r'[a-z]'));
|
||||
return value.runes.any((int rune) {
|
||||
var character = String.fromCharCode(rune);
|
||||
return character.toLowerCase() == character &&
|
||||
character.toUpperCase() != character;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import 'dart:convert';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
@@ -177,11 +179,14 @@ void changeIdDialog() {
|
||||
}
|
||||
|
||||
void changeWhiteList({Function()? callback}) async {
|
||||
var newWhiteList = (await bind.mainGetOption(key: 'whitelist')).split(',');
|
||||
var newWhiteListField = newWhiteList.join('\n');
|
||||
final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist);
|
||||
var newWhiteListField = curWhiteList == defaultOptionWhitelist
|
||||
? ''
|
||||
: curWhiteList.split(',').join('\n');
|
||||
var controller = TextEditingController(text: newWhiteListField);
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
final isOptFixed = isOptionFixed(kOptionWhitelist);
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("IP Whitelisting")),
|
||||
@@ -201,6 +206,7 @@ void changeWhiteList({Function()? callback}) async {
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
enabled: !isOptFixed,
|
||||
autofocus: true),
|
||||
),
|
||||
],
|
||||
@@ -214,45 +220,53 @@ void changeWhiteList({Function()? callback}) async {
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("Clear", onPressed: () async {
|
||||
await bind.mainSetOption(key: 'whitelist', value: '');
|
||||
callback?.call();
|
||||
close();
|
||||
}, isOutline: true),
|
||||
dialogButton(
|
||||
"OK",
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
newWhiteListField = controller.text.trim();
|
||||
var newWhiteList = "";
|
||||
if (newWhiteListField.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
// test ip
|
||||
final ipMatch = RegExp(
|
||||
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
||||
final ipv6Match = RegExp(
|
||||
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
|
||||
for (final ip in ips) {
|
||||
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
|
||||
msg = "${translate("Invalid IP")} $ip";
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
newWhiteList = ips.join(',');
|
||||
}
|
||||
await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
|
||||
if (!isOptFixed)
|
||||
dialogButton("Clear", onPressed: () async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||
callback?.call();
|
||||
close();
|
||||
},
|
||||
),
|
||||
}, isOutline: true),
|
||||
if (!isOptFixed)
|
||||
dialogButton(
|
||||
"OK",
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
newWhiteListField = controller.text.trim();
|
||||
var newWhiteList = "";
|
||||
if (newWhiteListField.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ips =
|
||||
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
// test ip
|
||||
final ipMatch = RegExp(
|
||||
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
||||
final ipv6Match = RegExp(
|
||||
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
|
||||
for (final ip in ips) {
|
||||
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
|
||||
msg = "${translate("Invalid IP")} $ip";
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
newWhiteList = ips.join(',');
|
||||
}
|
||||
if (newWhiteList.trim().isEmpty) {
|
||||
newWhiteList = defaultOptionWhitelist;
|
||||
}
|
||||
await bind.mainSetOption(
|
||||
key: kOptionWhitelist, value: newWhiteList);
|
||||
callback?.call();
|
||||
close();
|
||||
},
|
||||
),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
@@ -298,7 +312,7 @@ Future<String> changeDirectAccessPort(
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: () async {
|
||||
await bind.mainSetOption(
|
||||
key: 'direct-access-port', value: controller.text);
|
||||
key: kOptionDirectAccessPort, value: controller.text);
|
||||
close();
|
||||
}),
|
||||
],
|
||||
@@ -345,7 +359,7 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("OK", onPressed: () async {
|
||||
await bind.mainSetOption(
|
||||
key: 'auto-disconnect-timeout', value: controller.text);
|
||||
key: kOptionAutoDisconnectTimeout, value: controller.text);
|
||||
close();
|
||||
}),
|
||||
],
|
||||
@@ -367,6 +381,7 @@ class DialogTextField extends StatelessWidget {
|
||||
final FocusNode? focusNode;
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final int? maxLength;
|
||||
|
||||
static const kUsernameTitle = 'Username';
|
||||
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
||||
@@ -384,6 +399,7 @@ class DialogTextField extends StatelessWidget {
|
||||
this.hintText,
|
||||
this.keyboardType,
|
||||
this.inputFormatters,
|
||||
this.maxLength,
|
||||
required this.title,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
@@ -410,6 +426,7 @@ class DialogTextField extends StatelessWidget {
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -666,6 +683,8 @@ class PasswordWidget extends StatefulWidget {
|
||||
this.reRequestFocus = false,
|
||||
this.hintText,
|
||||
this.errorText,
|
||||
this.title,
|
||||
this.maxLength,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
@@ -673,6 +692,8 @@ class PasswordWidget extends StatefulWidget {
|
||||
final bool reRequestFocus;
|
||||
final String? hintText;
|
||||
final String? errorText;
|
||||
final String? title;
|
||||
final int? maxLength;
|
||||
|
||||
@override
|
||||
State<PasswordWidget> createState() => _PasswordWidgetState();
|
||||
@@ -716,7 +737,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DialogTextField(
|
||||
title: translate(DialogTextField.kPasswordTitle),
|
||||
title: translate(widget.title ?? DialogTextField.kPasswordTitle),
|
||||
hintText: translate(widget.hintText ?? 'Enter your password'),
|
||||
controller: widget.controller,
|
||||
prefixIcon: DialogTextField.kPasswordIcon,
|
||||
@@ -735,6 +756,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
obscureText: !_passwordVisible,
|
||||
errorText: widget.errorText,
|
||||
focusNode: _focusNode,
|
||||
maxLength: widget.maxLength,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1108,7 +1130,7 @@ void showRequestElevationDialog(
|
||||
errorText: errPwd.isEmpty ? null : errPwd.value,
|
||||
),
|
||||
],
|
||||
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
|
||||
).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
|
||||
).marginOnly(top: 10),
|
||||
],
|
||||
),
|
||||
@@ -1753,9 +1775,70 @@ void renameDialog(
|
||||
});
|
||||
}
|
||||
|
||||
void changeBot({Function()? callback}) async {
|
||||
if (bind.mainHasValidBotSync()) {
|
||||
await bind.mainSetOption(key: "bot", value: "");
|
||||
callback?.call();
|
||||
return;
|
||||
}
|
||||
String errorText = '';
|
||||
bool loading = false;
|
||||
final controller = TextEditingController();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
onVerify() async {
|
||||
final token = controller.text.trim();
|
||||
if (token == "") return;
|
||||
loading = true;
|
||||
errorText = '';
|
||||
setState(() {});
|
||||
final error = await bind.mainVerifyBot(token: token);
|
||||
if (error == "") {
|
||||
callback?.call();
|
||||
close();
|
||||
} else {
|
||||
errorText = translate(error);
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
final codeField = TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: translate('Token'),
|
||||
),
|
||||
);
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Telegram bot")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(translate("enable-bot-desc"),
|
||||
style: TextStyle(fontSize: 12))
|
||||
.marginOnly(bottom: 12),
|
||||
Row(children: [Expanded(child: codeField)]),
|
||||
if (errorText != '')
|
||||
Text(errorText, style: TextStyle(color: Colors.red))
|
||||
.marginOnly(top: 12),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
loading
|
||||
? CircularProgressIndicator()
|
||||
: dialogButton("OK", onPressed: onVerify),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void change2fa({Function()? callback}) async {
|
||||
if (bind.mainHasValid2FaSync()) {
|
||||
await bind.mainSetOption(key: "2fa", value: "");
|
||||
await bind.mainClearTrustedDevices();
|
||||
callback?.call();
|
||||
return;
|
||||
}
|
||||
@@ -1823,6 +1906,7 @@ void enter2FaDialog(
|
||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
final RxBool submitReady = false.obs;
|
||||
final RxBool trustThisDevice = false.obs;
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close, context) {
|
||||
@@ -1832,7 +1916,7 @@ void enter2FaDialog(
|
||||
}
|
||||
|
||||
submit() {
|
||||
gFFI.send2FA(sessionId, controller.text.trim());
|
||||
gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
@@ -1846,9 +1930,27 @@ void enter2FaDialog(
|
||||
onChanged: () => submitReady.value = codeField.isReady,
|
||||
);
|
||||
|
||||
final trustField = Obx(() => CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(translate("Trust this device")),
|
||||
value: trustThisDevice.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
trustThisDevice.value = value;
|
||||
},
|
||||
));
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('enter-2fa-title')),
|
||||
content: codeField,
|
||||
content: Column(
|
||||
children: [
|
||||
codeField,
|
||||
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
|
||||
trustField,
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton('Cancel',
|
||||
onPressed: cancel,
|
||||
@@ -2115,3 +2217,283 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
|
||||
VoidCallback onConfirm) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
close();
|
||||
onConfirm.call();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(content,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
textAlign: TextAlign.start),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 12),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void changeUnlockPinDialog(String oldPin, Function() callback) {
|
||||
final pinController = TextEditingController(text: oldPin);
|
||||
final confirmController = TextEditingController(text: oldPin);
|
||||
String? pinErrorText;
|
||||
String? confirmationErrorText;
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
pinErrorText = null;
|
||||
confirmationErrorText = null;
|
||||
final pin = pinController.text.trim();
|
||||
final confirm = confirmController.text.trim();
|
||||
if (pin != confirm) {
|
||||
setState(() {
|
||||
confirmationErrorText =
|
||||
translate('The confirmation is not identical.');
|
||||
});
|
||||
return;
|
||||
}
|
||||
final errorMsg = bind.mainSetUnlockPin(pin: pin);
|
||||
if (errorMsg != '') {
|
||||
setState(() {
|
||||
pinErrorText = translate(errorMsg);
|
||||
});
|
||||
return;
|
||||
}
|
||||
callback.call();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set PIN")),
|
||||
content: Column(
|
||||
children: [
|
||||
DialogTextField(
|
||||
title: 'PIN',
|
||||
controller: pinController,
|
||||
obscureText: true,
|
||||
errorText: pinErrorText,
|
||||
maxLength: maxLength,
|
||||
),
|
||||
DialogTextField(
|
||||
title: translate('Confirmation'),
|
||||
controller: confirmController,
|
||||
obscureText: true,
|
||||
errorText: confirmationErrorText,
|
||||
maxLength: maxLength,
|
||||
)
|
||||
],
|
||||
).marginOnly(bottom: 12),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void checkUnlockPinDialog(String correctPin, Function() passCallback) {
|
||||
final controller = TextEditingController();
|
||||
String? errorText;
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
final pin = controller.text.trim();
|
||||
if (correctPin != pin) {
|
||||
setState(() {
|
||||
errorText = translate('Wrong PIN');
|
||||
});
|
||||
return;
|
||||
}
|
||||
passCallback.call();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PasswordWidget(
|
||||
title: 'PIN',
|
||||
controller: controller,
|
||||
errorText: errorText,
|
||||
hintText: '',
|
||||
))
|
||||
],
|
||||
).marginOnly(bottom: 12),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void confrimDeleteTrustedDevicesDialog(
|
||||
RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
|
||||
CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
|
||||
() async {
|
||||
if (selectedDevices.isEmpty) return;
|
||||
if (selectedDevices.length == trustedDevices.length) {
|
||||
await bind.mainClearTrustedDevices();
|
||||
trustedDevices.clear();
|
||||
selectedDevices.clear();
|
||||
} else {
|
||||
final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
|
||||
await bind.mainRemoveTrustedDevices(json: json);
|
||||
trustedDevices.removeWhere((element) {
|
||||
return selectedDevices.contains(element.hwid);
|
||||
});
|
||||
selectedDevices.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void manageTrustedDeviceDialog() async {
|
||||
RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
|
||||
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Manage trusted devices")),
|
||||
content: trustedDevicesTable(trustedDevices, selectedDevices),
|
||||
actions: [
|
||||
Obx(() => dialogButton(translate("Delete"),
|
||||
onPressed: selectedDevices.isEmpty
|
||||
? null
|
||||
: () {
|
||||
confrimDeleteTrustedDevicesDialog(
|
||||
trustedDevices,
|
||||
selectedDevices,
|
||||
);
|
||||
},
|
||||
isOutline: false)
|
||||
.marginOnly(top: 12)),
|
||||
dialogButton(translate("Close"), onPressed: close, isOutline: true)
|
||||
.marginOnly(top: 12),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class TrustedDevice {
|
||||
late final Uint8List hwid;
|
||||
late final int time;
|
||||
late final String id;
|
||||
late final String name;
|
||||
late final String platform;
|
||||
|
||||
TrustedDevice.fromJson(Map<String, dynamic> json) {
|
||||
final hwidList = json['hwid'] as List<dynamic>;
|
||||
hwid = Uint8List.fromList(hwidList.cast<int>());
|
||||
time = json['time'];
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
platform = json['platform'];
|
||||
}
|
||||
|
||||
String daysRemaining() {
|
||||
final expiry = time + 90 * 24 * 60 * 60 * 1000;
|
||||
final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
|
||||
if (remaining < 0) {
|
||||
return '0';
|
||||
}
|
||||
return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
|
||||
}
|
||||
|
||||
static Future<List<TrustedDevice>> get() async {
|
||||
final List<TrustedDevice> devices = List.empty(growable: true);
|
||||
try {
|
||||
final devicesJson = await bind.mainGetTrustedDevices();
|
||||
if (devicesJson.isNotEmpty) {
|
||||
final devicesList = json.decode(devicesJson);
|
||||
if (devicesList is List) {
|
||||
for (var device in devicesList) {
|
||||
devices.add(TrustedDevice.fromJson(device));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
devices.sort((a, b) => b.time.compareTo(a.time));
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
|
||||
Widget trustedDevicesTable(
|
||||
RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
|
||||
RxBool selectAll = false.obs;
|
||||
setSelectAll() {
|
||||
if (selectedDevices.isNotEmpty &&
|
||||
selectedDevices.length == devices.length) {
|
||||
selectAll.value = true;
|
||||
} else {
|
||||
selectAll.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
devices.listen((_) {
|
||||
setSelectAll();
|
||||
});
|
||||
selectedDevices.listen((_) {
|
||||
setSelectAll();
|
||||
});
|
||||
return FittedBox(
|
||||
child: Obx(() => DataTable(
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Checkbox(
|
||||
value: selectAll.value,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
selectedDevices.clear();
|
||||
selectedDevices.addAll(devices.map((e) => e.hwid));
|
||||
} else {
|
||||
selectedDevices.clear();
|
||||
}
|
||||
},
|
||||
)),
|
||||
DataColumn(label: Text(translate('Platform'))),
|
||||
DataColumn(label: Text(translate('ID'))),
|
||||
DataColumn(label: Text(translate('Username'))),
|
||||
DataColumn(label: Text(translate('Days remaining'))),
|
||||
],
|
||||
rows: devices.map((device) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Checkbox(
|
||||
value: selectedDevices.contains(device.hwid),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
selectedDevices.remove(device.hwid);
|
||||
selectedDevices.add(device.hwid);
|
||||
} else {
|
||||
selectedDevices.remove(device.hwid);
|
||||
}
|
||||
},
|
||||
)),
|
||||
DataCell(Text(device.platform)),
|
||||
DataCell(Text(device.id)),
|
||||
DataCell(Text(device.name)),
|
||||
DataCell(Text(device.daysRemaining())),
|
||||
]);
|
||||
}).toList(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
// end
|
||||
switch (_currentState) {
|
||||
case GestureState.oneFingerPan:
|
||||
debugPrint("TwoFingerState.pan onEnd");
|
||||
debugPrint("OneFingerState.pan onEnd");
|
||||
if (onOneFingerPanEnd != null) {
|
||||
onOneFingerPanEnd!(_getDragEndDetails(d));
|
||||
}
|
||||
@@ -112,6 +112,8 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
};
|
||||
}
|
||||
|
||||
// FIXME: This debounce logic is not working properly.
|
||||
// If we move our finger very fast, we won't be able to detect the "oneFingerPan" event sometimes.
|
||||
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
||||
start(ScaleUpdateDetails d) {
|
||||
_currentState = GestureState.oneFingerPan;
|
||||
|
||||
@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
|
||||
String _failedMsg = '';
|
||||
String _url = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
@@ -455,7 +450,7 @@ Future<bool?> loginDialog() async {
|
||||
}
|
||||
if (isEmailVerification != null) {
|
||||
if (isMobile) {
|
||||
if (close != null) close(false);
|
||||
if (close != null) close(null);
|
||||
verificationCodeDialog(
|
||||
resp.user, resp.secret, isEmailVerification);
|
||||
} else {
|
||||
@@ -712,6 +707,11 @@ Future<bool?> verificationCodeDialog(
|
||||
dialogButton("Verify", onPressed: getOnSubmit()),
|
||||
]);
|
||||
});
|
||||
// For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
|
||||
// otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
|
||||
if (isMobile && res == true) {
|
||||
await UserModel.updateOtherModels();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/common/widgets/login.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
@@ -23,11 +24,6 @@ class _MyGroupState extends State<MyGroup> {
|
||||
RxString get searchUserText => gFFI.groupModel.searchUserText;
|
||||
static TextEditingController searchUserController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
@@ -35,6 +31,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||
return netWorkErrorWidget();
|
||||
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
@@ -48,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
|
||||
retry: null,
|
||||
close: () => gFFI.groupModel.groupLoadError.value = ''),
|
||||
Expanded(
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? _buildDesktop()
|
||||
: _buildMobile())
|
||||
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? _buildPortrait()
|
||||
: _buildLandscape())),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDesktop() {
|
||||
Widget _buildLandscape() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
@@ -85,14 +83,14 @@ class _MyGroupState extends State<MyGroup> {
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.groupModel.peers)),
|
||||
menuPadding: widget.menuPadding,
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobile() {
|
||||
Widget _buildPortrait() {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -117,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
getInitPeers: () => gFFI.groupModel.peers)),
|
||||
menuPadding: widget.menuPadding,
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -162,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
final listView = ListView.builder(
|
||||
shrinkWrap: isMobile,
|
||||
listView(bool isPortrait) => ListView.builder(
|
||||
shrinkWrap: isPortrait,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return (isDesktop || isWebDesktop)
|
||||
? listView
|
||||
: LimitedBox(maxHeight: maxHeight, child: listView);
|
||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||
? listView(false)
|
||||
: LimitedBox(maxHeight: maxHeight, child: listView(true)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (draggablePositions.chatWindow.isInvalid()) {
|
||||
draggablePositions.chatWindow.update(position);
|
||||
}
|
||||
return isIOS
|
||||
? IOSDraggable(
|
||||
position: position,
|
||||
position: draggablePositions.chatWindow,
|
||||
chatModel: chatModel,
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
position: position,
|
||||
position: draggablePositions.chatWindow,
|
||||
width: width,
|
||||
height: height,
|
||||
chatModel: chatModel,
|
||||
@@ -166,15 +171,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// floating buttons of back/home/recent actions for android
|
||||
class DraggableMobileActions extends StatelessWidget {
|
||||
DraggableMobileActions(
|
||||
{this.position = Offset.zero,
|
||||
this.onBackPressed,
|
||||
{this.onBackPressed,
|
||||
this.onRecentPressed,
|
||||
this.onHomePressed,
|
||||
this.onHidePressed,
|
||||
required this.position,
|
||||
required this.width,
|
||||
required this.height});
|
||||
required this.height,
|
||||
required this.scale});
|
||||
|
||||
final Offset position;
|
||||
final double scale;
|
||||
final DraggableKeyPosition position;
|
||||
final double width;
|
||||
final double height;
|
||||
final VoidCallback? onBackPressed;
|
||||
@@ -186,8 +193,8 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable(
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
width: scale * width,
|
||||
height: scale * height,
|
||||
builder: (_, onPanUpdate) {
|
||||
return GestureDetector(
|
||||
onPanUpdate: onPanUpdate,
|
||||
@@ -197,7 +204,8 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.accent.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.all(Radius.circular(15))),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(15 * scale))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
@@ -205,17 +213,20 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
onPressed: onBackPressed,
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
icon: const Icon(Icons.arrow_back)),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
iconSize: 24 * scale),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
onPressed: onHomePressed,
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
icon: const Icon(Icons.home)),
|
||||
icon: const Icon(Icons.home),
|
||||
iconSize: 24 * scale),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
onPressed: onRecentPressed,
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
icon: const Icon(Icons.more_horiz)),
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
iconSize: 24 * scale),
|
||||
const VerticalDivider(
|
||||
width: 0,
|
||||
thickness: 2,
|
||||
@@ -226,7 +237,8 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
onPressed: onHidePressed,
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
icon: const Icon(Icons.keyboard_arrow_down)),
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
iconSize: 24 * scale),
|
||||
],
|
||||
),
|
||||
)));
|
||||
@@ -234,12 +246,98 @@ class DraggableMobileActions extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DraggableKeyPosition {
|
||||
final String key;
|
||||
Offset _pos;
|
||||
late Debouncer<int> _debouncerStore;
|
||||
DraggableKeyPosition(this.key)
|
||||
: _pos = DraggablePositions.kInvalidDraggablePosition;
|
||||
|
||||
get pos => _pos;
|
||||
|
||||
_loadPosition(String k) {
|
||||
final value = bind.getLocalFlutterOption(k: k);
|
||||
if (value.isNotEmpty) {
|
||||
final parts = value.split(',');
|
||||
if (parts.length == 2) {
|
||||
return Offset(double.parse(parts[0]), double.parse(parts[1]));
|
||||
}
|
||||
}
|
||||
return DraggablePositions.kInvalidDraggablePosition;
|
||||
}
|
||||
|
||||
load() {
|
||||
_pos = _loadPosition(key);
|
||||
_debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
|
||||
onChanged: (v) => _store(), initialValue: 0);
|
||||
}
|
||||
|
||||
update(Offset pos) {
|
||||
_pos = pos;
|
||||
_triggerStore();
|
||||
}
|
||||
|
||||
// Adjust position to keep it in the screen
|
||||
// Only used for desktop and web desktop
|
||||
tryAdjust(double w, double h, double scale) {
|
||||
final size = MediaQuery.of(Get.context!).size;
|
||||
w = w * scale;
|
||||
h = h * scale;
|
||||
double x = _pos.dx;
|
||||
double y = _pos.dy;
|
||||
if (x + w > size.width) {
|
||||
x = size.width - w;
|
||||
}
|
||||
final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
|
||||
if (y + h > (size.height - tabBarHeight)) {
|
||||
y = size.height - tabBarHeight - h;
|
||||
}
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
}
|
||||
if (x != _pos.dx || y != _pos.dy) {
|
||||
update(Offset(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
isInvalid() {
|
||||
return _pos == DraggablePositions.kInvalidDraggablePosition;
|
||||
}
|
||||
|
||||
_triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
|
||||
_store() {
|
||||
bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
|
||||
}
|
||||
}
|
||||
|
||||
class DraggablePositions {
|
||||
static const kChatWindow = 'draggablePositionChat';
|
||||
static const kMobileActions = 'draggablePositionMobile';
|
||||
static const kIOSDraggable = 'draggablePositionIOS';
|
||||
|
||||
static const kInvalidDraggablePosition = Offset(-999999, -999999);
|
||||
final chatWindow = DraggableKeyPosition(kChatWindow);
|
||||
final mobileActions = DraggableKeyPosition(kMobileActions);
|
||||
final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
|
||||
|
||||
load() {
|
||||
chatWindow.load();
|
||||
mobileActions.load();
|
||||
iOSDraggable.load();
|
||||
}
|
||||
}
|
||||
|
||||
DraggablePositions draggablePositions = DraggablePositions();
|
||||
|
||||
class Draggable extends StatefulWidget {
|
||||
const Draggable(
|
||||
Draggable(
|
||||
{Key? key,
|
||||
this.checkKeyboard = false,
|
||||
this.checkScreenSize = false,
|
||||
this.position = Offset.zero,
|
||||
required this.position,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.chatModel,
|
||||
@@ -248,55 +346,53 @@ class Draggable extends StatefulWidget {
|
||||
|
||||
final bool checkKeyboard;
|
||||
final bool checkScreenSize;
|
||||
final Offset position;
|
||||
final DraggableKeyPosition position;
|
||||
final double width;
|
||||
final double height;
|
||||
final ChatModel? chatModel;
|
||||
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DraggableState();
|
||||
State<StatefulWidget> createState() => _DraggableState(chatModel);
|
||||
}
|
||||
|
||||
class _DraggableState extends State<Draggable> {
|
||||
late Offset _position;
|
||||
late ChatModel? _chatModel;
|
||||
bool _keyboardVisible = false;
|
||||
double _saveHeight = 0;
|
||||
double _lastBottomHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_position = widget.position;
|
||||
_chatModel = widget.chatModel;
|
||||
_DraggableState(ChatModel? chatModel) {
|
||||
_chatModel = chatModel;
|
||||
}
|
||||
|
||||
get position => widget.position.pos;
|
||||
|
||||
void onPanUpdate(DragUpdateDetails d) {
|
||||
final offset = d.delta;
|
||||
final size = MediaQuery.of(context).size;
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
|
||||
if (_position.dx + offset.dx + widget.width > size.width) {
|
||||
if (position.dx + offset.dx + widget.width > size.width) {
|
||||
x = size.width - widget.width;
|
||||
} else if (_position.dx + offset.dx < 0) {
|
||||
} else if (position.dx + offset.dx < 0) {
|
||||
x = 0;
|
||||
} else {
|
||||
x = _position.dx + offset.dx;
|
||||
x = position.dx + offset.dx;
|
||||
}
|
||||
|
||||
if (_position.dy + offset.dy + widget.height > size.height) {
|
||||
if (position.dy + offset.dy + widget.height > size.height) {
|
||||
y = size.height - widget.height;
|
||||
} else if (_position.dy + offset.dy < 0) {
|
||||
} else if (position.dy + offset.dy < 0) {
|
||||
y = 0;
|
||||
} else {
|
||||
y = _position.dy + offset.dy;
|
||||
y = position.dy + offset.dy;
|
||||
}
|
||||
setState(() {
|
||||
_position = Offset(x, y);
|
||||
widget.position.update(Offset(x, y));
|
||||
});
|
||||
_chatModel?.setChatWindowPosition(_position);
|
||||
_chatModel?.setChatWindowPosition(position);
|
||||
}
|
||||
|
||||
checkScreenSize() {}
|
||||
@@ -307,13 +403,13 @@ class _DraggableState extends State<Draggable> {
|
||||
|
||||
// save
|
||||
if (!_keyboardVisible && currentVisible) {
|
||||
_saveHeight = _position.dy;
|
||||
_saveHeight = position.dy;
|
||||
}
|
||||
|
||||
// reset
|
||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||
setState(() {
|
||||
_position = Offset(_position.dx, _saveHeight);
|
||||
widget.position.update(Offset(position.dx, _saveHeight));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,10 +417,10 @@ class _DraggableState extends State<Draggable> {
|
||||
if (_keyboardVisible && currentVisible) {
|
||||
final sumHeight = bottomHeight + widget.height;
|
||||
final contextHeight = MediaQuery.of(context).size.height;
|
||||
if (sumHeight + _position.dy > contextHeight) {
|
||||
if (sumHeight + position.dy > contextHeight) {
|
||||
final y = contextHeight - sumHeight;
|
||||
setState(() {
|
||||
_position = Offset(_position.dx, y);
|
||||
widget.position.update(Offset(position.dx, y));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -343,8 +439,8 @@ class _DraggableState extends State<Draggable> {
|
||||
}
|
||||
return Stack(children: [
|
||||
Positioned(
|
||||
top: _position.dy,
|
||||
left: _position.dx,
|
||||
top: position.dy,
|
||||
left: position.dx,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: widget.builder(context, onPanUpdate))
|
||||
@@ -355,25 +451,25 @@ class _DraggableState extends State<Draggable> {
|
||||
class IOSDraggable extends StatefulWidget {
|
||||
const IOSDraggable(
|
||||
{Key? key,
|
||||
this.position = Offset.zero,
|
||||
this.chatModel,
|
||||
required this.position,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.builder})
|
||||
: super(key: key);
|
||||
|
||||
final Offset position;
|
||||
final DraggableKeyPosition position;
|
||||
final ChatModel? chatModel;
|
||||
final double width;
|
||||
final double height;
|
||||
final Widget Function(BuildContext) builder;
|
||||
|
||||
@override
|
||||
IOSDraggableState createState() => IOSDraggableState();
|
||||
IOSDraggableState createState() =>
|
||||
IOSDraggableState(chatModel, width, height);
|
||||
}
|
||||
|
||||
class IOSDraggableState extends State<IOSDraggable> {
|
||||
late Offset _position;
|
||||
late ChatModel? _chatModel;
|
||||
late double _width;
|
||||
late double _height;
|
||||
@@ -381,28 +477,27 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
double _saveHeight = 0;
|
||||
double _lastBottomHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_position = widget.position;
|
||||
_chatModel = widget.chatModel;
|
||||
_width = widget.width;
|
||||
_height = widget.height;
|
||||
IOSDraggableState(ChatModel? chatModel, double w, double h) {
|
||||
_chatModel = chatModel;
|
||||
_width = w;
|
||||
_height = h;
|
||||
}
|
||||
|
||||
DraggableKeyPosition get position => widget.position;
|
||||
|
||||
checkKeyboard() {
|
||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
final currentVisible = bottomHeight != 0;
|
||||
|
||||
// save
|
||||
if (!_keyboardVisible && currentVisible) {
|
||||
_saveHeight = _position.dy;
|
||||
_saveHeight = position.pos.dy;
|
||||
}
|
||||
|
||||
// reset
|
||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||
setState(() {
|
||||
_position = Offset(_position.dx, _saveHeight);
|
||||
position.update(Offset(position.pos.dx, _saveHeight));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -410,10 +505,10 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
if (_keyboardVisible && currentVisible) {
|
||||
final sumHeight = bottomHeight + _height;
|
||||
final contextHeight = MediaQuery.of(context).size.height;
|
||||
if (sumHeight + _position.dy > contextHeight) {
|
||||
if (sumHeight + position.pos.dy > contextHeight) {
|
||||
final y = contextHeight - sumHeight;
|
||||
setState(() {
|
||||
_position = Offset(_position.dx, y);
|
||||
position.update(Offset(position.pos.dx, y));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -428,14 +523,14 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: _position.dx,
|
||||
top: _position.dy,
|
||||
left: position.pos.dx,
|
||||
top: position.pos.dy,
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
setState(() {
|
||||
_position += details.delta;
|
||||
position.update(position.pos + details.delta);
|
||||
});
|
||||
_chatModel?.setChatWindowPosition(_position);
|
||||
_chatModel?.setChatWindowPosition(position.pos);
|
||||
},
|
||||
child: Material(
|
||||
child: Container(
|
||||
@@ -491,8 +586,10 @@ class QualityMonitor extends StatelessWidget {
|
||||
children: [
|
||||
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
|
||||
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
|
||||
// let delay be 0 if fps is 0
|
||||
_row(
|
||||
"Delay", "${qualityMonitorModel.data.delay ?? '-'}ms",
|
||||
"Delay",
|
||||
"${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
|
||||
rightColor: Colors.green),
|
||||
_row("Target Bitrate",
|
||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -22,6 +23,8 @@ enum PeerUiType { grid, tile, list }
|
||||
|
||||
final peerCardUiType = PeerUiType.grid.obs;
|
||||
|
||||
bool? hideUsernameOnCard;
|
||||
|
||||
class _PeerCard extends StatefulWidget {
|
||||
final Peer peer;
|
||||
final PeerTabIndex tab;
|
||||
@@ -51,42 +54,44 @@ class _PeerCardState extends State<_PeerCard>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
if (isDesktop || isWebDesktop) {
|
||||
return _buildDesktop();
|
||||
} else {
|
||||
return _buildMobile();
|
||||
}
|
||||
return Obx(() =>
|
||||
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
|
||||
}
|
||||
|
||||
Widget _buildMobile() {
|
||||
final peer = super.widget.peer;
|
||||
Widget gestureDetector({required Widget child}) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final peer = super.widget.peer;
|
||||
return GestureDetector(
|
||||
onDoubleTap: peerTabModel.multiSelectionMode
|
||||
? null
|
||||
: () => widget.connect(context, peer.id),
|
||||
onTap: () {
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
peerTabModel.select(peer);
|
||||
} else {
|
||||
if (isMobile) {
|
||||
widget.connect(context, peer.id);
|
||||
} else {
|
||||
peerTabModel.select(peer);
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongPress: () => peerTabModel.select(peer),
|
||||
child: child);
|
||||
}
|
||||
|
||||
Widget _buildPortrait() {
|
||||
final peer = super.widget.peer;
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 2),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
peerTabModel.select(peer);
|
||||
} else {
|
||||
if (!isWebDesktop) {
|
||||
connectInPeerTab(context, peer, widget.tab);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDoubleTap: isWebDesktop
|
||||
? () => connectInPeerTab(context, peer, widget.tab)
|
||||
: null,
|
||||
onLongPress: () {
|
||||
peerTabModel.select(peer);
|
||||
},
|
||||
child: gestureDetector(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
||||
child: _buildPeerTile(context, peer, null)),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildDesktop() {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
Widget _buildLandscape() {
|
||||
final peer = super.widget.peer;
|
||||
var deco = Rx<BoxDecoration?>(
|
||||
BoxDecoration(
|
||||
@@ -115,33 +120,27 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onDoubleTap:
|
||||
peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
|
||||
? null
|
||||
: () => widget.connect(context, peer.id),
|
||||
onTap: () => peerTabModel.select(peer),
|
||||
onLongPress: () => peerTabModel.select(peer),
|
||||
child: gestureDetector(
|
||||
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
||||
? _buildPeerCard(context, peer, deco)
|
||||
: _buildPeerTile(context, peer, deco))),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeerTile(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||
final name =
|
||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
makeChild(bool isPortrait, Peer peer) {
|
||||
final name = hideUsernameOnCard == true
|
||||
? peer.hostname
|
||||
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
final greyStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||
final child = Row(
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||
borderRadius: isMobile
|
||||
borderRadius: isPortrait
|
||||
? BorderRadius.circular(_tileRadius)
|
||||
: BorderRadius.only(
|
||||
topLeft: Radius.circular(_tileRadius),
|
||||
@@ -149,11 +148,11 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
width: isMobile ? 50 : 42,
|
||||
height: isMobile ? 50 : null,
|
||||
width: isPortrait ? 50 : 42,
|
||||
height: isPortrait ? 50 : null,
|
||||
child: Stack(
|
||||
children: [
|
||||
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
||||
getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
|
||||
.paddingAll(6),
|
||||
if (_shouldBuildPasswordIcon(peer))
|
||||
Positioned(
|
||||
@@ -178,19 +177,19 @@ class _PeerCardState extends State<_PeerCard>
|
||||
child: Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
getOnline(isMobile ? 4 : 8, peer.online),
|
||||
getOnline(isPortrait ? 4 : 8, peer.online),
|
||||
Expanded(
|
||||
child: Text(
|
||||
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
)),
|
||||
]).marginOnly(top: isMobile ? 0 : 2),
|
||||
]).marginOnly(top: isPortrait ? 0 : 2),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: isMobile ? null : greyStyle,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -198,53 +197,65 @@ class _PeerCardState extends State<_PeerCard>
|
||||
],
|
||||
).marginOnly(top: 2),
|
||||
),
|
||||
isMobile
|
||||
? checkBoxOrActionMoreMobile(peer)
|
||||
: checkBoxOrActionMoreDesktop(peer, isTile: true),
|
||||
isPortrait
|
||||
? checkBoxOrActionMorePortrait(peer)
|
||||
: checkBoxOrActionMoreLandscape(peer, isTile: true),
|
||||
],
|
||||
).paddingOnly(left: 10.0, top: 3.0),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeerTile(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||
hideUsernameOnCard ??=
|
||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||
final colors = _frontN(peer.tags, 25)
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
message: !(isDesktop || isWebDesktop)
|
||||
? ''
|
||||
: peer.tags.isNotEmpty
|
||||
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
||||
: '',
|
||||
child: Stack(children: [
|
||||
deco == null
|
||||
? child
|
||||
: Obx(
|
||||
() => Container(
|
||||
Obx(
|
||||
() => deco == null
|
||||
? makeChild(stateGlobal.isPortrait.isTrue, peer)
|
||||
: Container(
|
||||
foregroundDecoration: deco.value,
|
||||
child: child,
|
||||
child: makeChild(stateGlobal.isPortrait.isTrue, peer),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (colors.isNotEmpty)
|
||||
Positioned(
|
||||
top: 2,
|
||||
right: isMobile ? 20 : 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
)
|
||||
Obx(() => Positioned(
|
||||
top: 2,
|
||||
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
))
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeerCard(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
||||
final name =
|
||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
hideUsernameOnCard ??=
|
||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||
final name = hideUsernameOnCard == true
|
||||
? peer.hostname
|
||||
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
final child = Card(
|
||||
color: Colors.transparent,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
// to-do: memory leak here, more investigation needed.
|
||||
// Continious rebuilds of `Obx()` will cause memory leak here.
|
||||
// The simple demo does not have this issue.
|
||||
child: Obx(
|
||||
() => Container(
|
||||
foregroundDecoration: deco.value,
|
||||
@@ -308,7 +319,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
)),
|
||||
]).paddingSymmetric(vertical: 8)),
|
||||
checkBoxOrActionMoreDesktop(peer, isTile: false),
|
||||
checkBoxOrActionMoreLandscape(peer, isTile: false),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12.0),
|
||||
)
|
||||
@@ -354,7 +365,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
}
|
||||
}
|
||||
|
||||
Widget checkBoxOrActionMoreMobile(Peer peer) {
|
||||
Widget checkBoxOrActionMorePortrait(Peer peer) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
@@ -382,7 +393,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
}
|
||||
}
|
||||
|
||||
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) {
|
||||
Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
@@ -628,8 +639,8 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
|
||||
@protected
|
||||
Future<bool> _isForceAlwaysRelay(String id) async {
|
||||
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))
|
||||
.isNotEmpty;
|
||||
return option2bool(kOptionForceAlwaysRelay,
|
||||
(await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -868,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -887,7 +898,7 @@ class RecentPeerCard extends BasePeerCard {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
if (isDesktop || isWebDesktop) {
|
||||
if (isMobile || isDesktop || isWebDesktop) {
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
}
|
||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||
@@ -927,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -943,7 +954,7 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
if (isDesktop || isWebDesktop) {
|
||||
if (isMobile || isDesktop || isWebDesktop) {
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
}
|
||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||
@@ -980,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -1033,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1048,7 +1059,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
}
|
||||
if (gFFI.abModel.current.canWrite()) {
|
||||
menuItems.add(MenuEntryDivider());
|
||||
if (isDesktop || isWebDesktop) {
|
||||
if (isMobile || isDesktop || isWebDesktop) {
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
}
|
||||
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
||||
@@ -1165,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
_transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1195,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
}
|
||||
|
||||
void _rdpDialog(String id) async {
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
|
||||
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
|
||||
final portController = TextEditingController(text: port);
|
||||
@@ -1249,54 +1261,54 @@ void _rdpDialog(String id) async {
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
Row(
|
||||
children: [
|
||||
(isDesktop || isWebDesktop)
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: (isDesktop || isWebDesktop)
|
||||
? null
|
||||
: translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
|
||||
Row(
|
||||
children: [
|
||||
(isDesktop || isWebDesktop)
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: (isDesktop || isWebDesktop)
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility))),
|
||||
controller: passwordController,
|
||||
)),
|
||||
),
|
||||
],
|
||||
)
|
||||
labelText:
|
||||
isDesktop ? null : translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
maxLength: maxLength,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
isDesktop ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () =>
|
||||
secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility))),
|
||||
controller: passwordController,
|
||||
)),
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -74,9 +75,14 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
];
|
||||
RelativeRect? mobileTabContextMenuPos;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
|
||||
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||
|
||||
_PeerTabPageState() {
|
||||
_loadLocalOptions();
|
||||
}
|
||||
|
||||
void _loadLocalOptions() {
|
||||
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||
if (uiType != '') {
|
||||
peerCardUiType.value = int.parse(uiType) == 0
|
||||
? PeerUiType.grid
|
||||
@@ -85,8 +91,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
: PeerUiType.list;
|
||||
}
|
||||
hideAbTagsPanel.value =
|
||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
||||
super.initState();
|
||||
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
|
||||
}
|
||||
|
||||
Future<void> handleTabSelection(int tabIndex) async {
|
||||
@@ -103,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Widget build(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget selectionWrap(Widget widget) {
|
||||
return model.multiSelectionMode ? createMultiSelectionBar() : widget;
|
||||
return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
|
||||
}
|
||||
|
||||
return Column(
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Container(
|
||||
padding: (isDesktop || isWebDesktop)
|
||||
? null
|
||||
: EdgeInsets.symmetric(horizontal: 2),
|
||||
child: selectionWrap(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
visibleContextMenuListener(_createSwitchBar(context))),
|
||||
if (isMobile)
|
||||
..._mobileRightActions(context)
|
||||
else
|
||||
..._desktopRightActions(context)
|
||||
],
|
||||
)),
|
||||
),
|
||||
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
|
||||
Obx(() => SizedBox(
|
||||
height: 32,
|
||||
child: Container(
|
||||
padding: stateGlobal.isPortrait.isTrue
|
||||
? EdgeInsets.symmetric(horizontal: 2)
|
||||
: null,
|
||||
child: selectionWrap(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: visibleContextMenuListener(
|
||||
_createSwitchBar(context))),
|
||||
if (stateGlobal.isPortrait.isTrue)
|
||||
..._portraitRightActions(context)
|
||||
else
|
||||
..._landscapeRightActions(context)
|
||||
],
|
||||
)),
|
||||
),
|
||||
).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
|
||||
_createPeersView(),
|
||||
],
|
||||
);
|
||||
@@ -173,11 +178,13 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
child: Icon(model.tabIcon(t), color: color)
|
||||
.paddingSymmetric(horizontal: 4),
|
||||
).paddingSymmetric(horizontal: 4),
|
||||
onTap: () async {
|
||||
await handleTabSelection(t);
|
||||
await bind.setLocalFlutterOption(
|
||||
k: PeerTabModel.kPeerTabIndex, v: t.toString());
|
||||
},
|
||||
onTap: isOptionFixed(kOptionPeerTabIndex)
|
||||
? null
|
||||
: () async {
|
||||
await handleTabSelection(t);
|
||||
await bind.setLocalFlutterOption(
|
||||
k: kOptionPeerTabIndex, v: t.toString());
|
||||
},
|
||||
onHover: (value) => hover.value = value,
|
||||
),
|
||||
)));
|
||||
@@ -265,17 +272,22 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
if (!model.isEnabled[i]) continue;
|
||||
items.add(PopupMenuItem(
|
||||
height: kMinInteractiveDimension * 0.8,
|
||||
onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
||||
onTap: isOptVisiableFixed
|
||||
? null
|
||||
: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
||||
enabled: !isOptVisiableFixed,
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: model.isVisibleEnabled[i],
|
||||
onChanged: (_) {
|
||||
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}),
|
||||
onChanged: isOptVisiableFixed
|
||||
? null
|
||||
: (_) {
|
||||
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}),
|
||||
Expanded(child: Text(model.tabTooltip(i))),
|
||||
],
|
||||
),
|
||||
@@ -288,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
}
|
||||
|
||||
Widget visibleContextMenuListener(Widget child) {
|
||||
if (isMobile) {
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
return GestureDetector(
|
||||
onLongPressDown: (e) {
|
||||
final x = e.globalPosition.dx;
|
||||
@@ -332,8 +344,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
currentValue: model.isVisibleEnabled[tabIndex],
|
||||
setter: (show) async {
|
||||
model.setTabVisible(tabIndex, show);
|
||||
cancelFunc();
|
||||
}));
|
||||
// Do not hide the current menu (checkbox)
|
||||
// cancelFunc();
|
||||
},
|
||||
enabled: (!isOptVisiableFixed).obs));
|
||||
}
|
||||
return mod_menu.PopupMenu(
|
||||
items: menu
|
||||
@@ -348,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
.toList());
|
||||
}
|
||||
|
||||
Widget createMultiSelectionBar() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget createMultiSelectionBar(PeerTabModel model) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -367,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Row(
|
||||
children: [
|
||||
selectionCount(model.selectedPeers.length),
|
||||
selectAll(),
|
||||
selectAll(model),
|
||||
closeSelection(),
|
||||
],
|
||||
)
|
||||
@@ -388,9 +401,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
final peers = model.selectedPeers;
|
||||
switch (model.currentTab) {
|
||||
case 0:
|
||||
peers.map((p) async {
|
||||
for (var p in peers) {
|
||||
await bind.mainRemovePeer(id: p.id);
|
||||
}).toList();
|
||||
}
|
||||
await bind.mainLoadRecentPeers();
|
||||
break;
|
||||
case 1:
|
||||
@@ -402,9 +415,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
await bind.mainLoadFavPeers();
|
||||
break;
|
||||
case 2:
|
||||
peers.map((p) async {
|
||||
for (var p in peers) {
|
||||
await bind.mainRemoveDiscovered(id: p.id);
|
||||
}).toList();
|
||||
}
|
||||
await bind.mainLoadLanPeers();
|
||||
break;
|
||||
case 3:
|
||||
@@ -443,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
showToast(translate('Successful'));
|
||||
},
|
||||
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -464,7 +477,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
model.setMultiSelectionMode(false);
|
||||
},
|
||||
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -487,7 +500,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
});
|
||||
},
|
||||
child: Icon(Icons.tag))
|
||||
.marginOnly(left: isMobile ? 11 : 6),
|
||||
.marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -498,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget selectAll() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget selectAll(PeerTabModel model) {
|
||||
return Offstage(
|
||||
offstage:
|
||||
model.selectedPeers.length >= model.currentTabCachedPeers.length,
|
||||
@@ -529,7 +541,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Widget _toggleTags() {
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Toggle tags'),
|
||||
toolTip: translate('Toggle Tags'),
|
||||
hoverableWhenfalse: hideAbTagsPanel,
|
||||
child: Icon(
|
||||
Icons.tag_rounded,
|
||||
@@ -537,15 +549,16 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
),
|
||||
onTap: () async {
|
||||
await bind.mainSetLocalOption(
|
||||
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
|
||||
key: kOptionHideAbTagsPanel,
|
||||
value: hideAbTagsPanel.value ? defaultOptionNo : "Y");
|
||||
hideAbTagsPanel.value = !hideAbTagsPanel.value;
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _desktopRightActions(BuildContext context) {
|
||||
List<Widget> _landscapeRightActions(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
return [
|
||||
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
|
||||
const PeerSearchBar().marginOnly(right: 13),
|
||||
_createRefresh(
|
||||
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
||||
_createRefresh(
|
||||
@@ -566,7 +579,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _mobileRightActions(BuildContext context) {
|
||||
List<Widget> _portraitRightActions(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
|
||||
@@ -687,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
baseOffset: 0,
|
||||
extentOffset: peerSearchTextController.value.text.length);
|
||||
});
|
||||
return Container(
|
||||
width: isMobile ? 120 : 140,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Obx(() => Row(
|
||||
return Obx(() => Container(
|
||||
width: stateGlobal.isPortrait.isTrue ? 120 : 140,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
@@ -754,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
);
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,25 +803,38 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: getRadio<PeerUiType>(
|
||||
Text(
|
||||
translate(types.indexOf(e) == 0
|
||||
Tooltip(
|
||||
message: translate(types.indexOf(e) == 0
|
||||
? 'Big tiles'
|
||||
: types.indexOf(e) == 1
|
||||
? 'Small tiles'
|
||||
: 'List'),
|
||||
style: style),
|
||||
child: Icon(
|
||||
e == PeerUiType.grid
|
||||
? Icons.grid_view_rounded
|
||||
: e == PeerUiType.list
|
||||
? Icons.view_list_rounded
|
||||
: Icons.view_agenda_rounded,
|
||||
size: 18,
|
||||
)),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
dense: true,
|
||||
isOptionFixed(kOptionPeerCardUiType)
|
||||
? null
|
||||
: (PeerUiType? v) async {
|
||||
if (v != null) {
|
||||
peerCardUiType.value = v;
|
||||
setState(() {});
|
||||
await bind.setLocalFlutterOption(
|
||||
k: kOptionPeerCardUiType,
|
||||
v: peerCardUiType.value.index.toString(),
|
||||
);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
))));
|
||||
}
|
||||
@@ -820,7 +846,7 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
child: Icon(
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? Icons.grid_view_rounded
|
||||
: peerCardUiType.value == PeerUiType.tile
|
||||
: peerCardUiType.value == PeerUiType.list
|
||||
? Icons.view_list_rounded
|
||||
: Icons.view_agenda_rounded,
|
||||
size: 18,
|
||||
@@ -847,16 +873,18 @@ class PeerSortDropdown extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
@override
|
||||
void initState() {
|
||||
_PeerSortDropdownState() {
|
||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||
peerSort.value = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: "peer-sorting",
|
||||
v: peerSort.value,
|
||||
);
|
||||
_loadLocalOptions();
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _loadLocalOptions() {
|
||||
peerSort.value = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: kOptionPeerSorting,
|
||||
v: peerSort.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -882,7 +910,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
if (v != null) {
|
||||
peerSort.value = v;
|
||||
await bind.setLocalFlutterOption(
|
||||
k: "peer-sorting",
|
||||
k: kOptionPeerSorting,
|
||||
v: peerSort.value,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import 'dart:collection';
|
||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package: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';
|
||||
@@ -41,14 +43,26 @@ class LoadEvent {
|
||||
static const String group = 'load_group_peers';
|
||||
}
|
||||
|
||||
class PeersModelName {
|
||||
static const String recent = 'recent peer';
|
||||
static const String favorite = 'fav peer';
|
||||
static const String lan = 'discovered peer';
|
||||
static const String addressBook = 'address book peer';
|
||||
static const String group = 'group peer';
|
||||
}
|
||||
|
||||
/// for peer search text, global obs value
|
||||
final peerSearchText = "".obs;
|
||||
|
||||
/// for peer sort, global obs value
|
||||
final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs;
|
||||
RxString? _peerSort;
|
||||
RxString get peerSort {
|
||||
_peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||
return _peerSort!;
|
||||
}
|
||||
|
||||
// list for listener
|
||||
final obslist = [peerSearchText, peerSort].obs;
|
||||
RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
|
||||
|
||||
final peerSearchTextController =
|
||||
TextEditingController(text: peerSearchText.value);
|
||||
@@ -70,7 +84,8 @@ class _PeersView extends StatefulWidget {
|
||||
}
|
||||
|
||||
/// State for the peer widget.
|
||||
class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
class _PeersViewState extends State<_PeersView>
|
||||
with WindowListener, WidgetsBindingObserver {
|
||||
static const int _maxQueryCount = 3;
|
||||
final HashMap<String, String> _emptyMessages = HashMap.from({
|
||||
LoadEvent.recent: 'empty_recent_tip',
|
||||
@@ -82,20 +97,11 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
final _curPeers = <String>{};
|
||||
var _lastChangeTime = DateTime.now();
|
||||
var _lastQueryPeers = <String>{};
|
||||
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
|
||||
var _lastQueryTime = DateTime.now();
|
||||
var _lastWindowRestoreTime = DateTime.now();
|
||||
var _queryCount = 0;
|
||||
var _exit = false;
|
||||
|
||||
late final mobileWidth = () {
|
||||
const minWidth = 320.0;
|
||||
final windowWidth = MediaQuery.of(context).size.width;
|
||||
var width = windowWidth - 2 * space;
|
||||
if (windowWidth > minWidth + 2 * space) {
|
||||
final n = (windowWidth / (minWidth + 2 * space)).floor();
|
||||
width = windowWidth / n - 2 * space;
|
||||
}
|
||||
return width;
|
||||
}();
|
||||
bool _isActive = true;
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@@ -106,12 +112,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
windowManager.removeListener(this);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_exit = true;
|
||||
super.dispose();
|
||||
}
|
||||
@@ -119,17 +127,61 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
_queryCount = 0;
|
||||
_isActive = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
// We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
|
||||
// Maybe it's a bug of the window manager, but the source code seems to be correct.
|
||||
//
|
||||
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
|
||||
// we need the following comparison to ensure that `_isActive` is true in the end.
|
||||
if (isWindows &&
|
||||
DateTime.now().difference(_lastWindowRestoreTime) <
|
||||
const Duration(milliseconds: 300)) {
|
||||
return;
|
||||
}
|
||||
_queryCount = _maxQueryCount;
|
||||
_isActive = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowRestore() {
|
||||
// Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
|
||||
// But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
|
||||
if (!isWindows) return;
|
||||
_queryCount = 0;
|
||||
_isActive = true;
|
||||
_lastWindowRestoreTime = DateTime.now();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
_queryCount = _maxQueryCount;
|
||||
// Window minimize also triggers `onWindowBlur()`.
|
||||
}
|
||||
|
||||
// This function is required for mobile.
|
||||
// `onWindowFocus` works fine for desktop.
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (isDesktop || isWebDesktop) return;
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_isActive = true;
|
||||
_queryCount = 0;
|
||||
} else if (state == AppLifecycleState.inactive) {
|
||||
_isActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider<Peers>(
|
||||
create: (context) => widget.peers,
|
||||
// We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
|
||||
// Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
|
||||
// Simple demo can reproduce this issue.
|
||||
return ChangeNotifierProvider<Peers>.value(
|
||||
value: widget.peers,
|
||||
child: Consumer<Peers>(builder: (context, peers, child) {
|
||||
if (peers.peers.isEmpty) {
|
||||
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
||||
@@ -183,68 +235,59 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
var peers = snapshot.data!;
|
||||
if (peers.length > 1000) peers = peers.sublist(0, 1000);
|
||||
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
|
||||
buildOnePeer(Peer peer) {
|
||||
buildOnePeer(Peer peer, bool isPortrait) {
|
||||
final visibilityChild = VisibilityDetector(
|
||||
key: ValueKey(_cardId(peer.id)),
|
||||
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 || isWebDesktop)
|
||||
? Obx(
|
||||
() => SizedBox(
|
||||
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
|
||||
: peerCardUiType.value != PeerUiType.list
|
||||
? 42
|
||||
: 45,
|
||||
child: visibilityChild,
|
||||
),
|
||||
)
|
||||
: SizedBox(width: mobileWidth, child: visibilityChild);
|
||||
return !isPortrait
|
||||
? Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? Container(height: 45, child: visibilityChild)
|
||||
: peerCardUiType.value == PeerUiType.grid
|
||||
? SizedBox(
|
||||
width: 220, height: 140, child: visibilityChild)
|
||||
: SizedBox(
|
||||
width: 220, height: 42, child: visibilityChild))
|
||||
: Container(child: visibilityChild);
|
||||
}
|
||||
|
||||
final Widget child;
|
||||
if (isMobile) {
|
||||
child = DynamicGridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2, crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child = DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2, crossAxisSpacing: space),
|
||||
// We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
|
||||
// Continious rebuilds of `ListView.builder` will cause memory leak.
|
||||
// Simple demo can reproduce this issue.
|
||||
final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? ListView.builder(
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return buildOnePeer(peers[index], true).marginOnly(
|
||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||
},
|
||||
)
|
||||
: peerCardUiType.value == PeerUiType.list
|
||||
? ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false).marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
},
|
||||
)
|
||||
: DynamicGridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false);
|
||||
}));
|
||||
|
||||
if (updateEvent == UpdateEvent.load) {
|
||||
_curPeers.clear();
|
||||
@@ -265,10 +308,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
return body;
|
||||
}
|
||||
|
||||
final _queryInterval = const Duration(seconds: 20);
|
||||
var _queryInterval = const Duration(seconds: 20);
|
||||
|
||||
void _startCheckOnlines() {
|
||||
() async {
|
||||
final p = await bind.mainIsUsingPublicServer();
|
||||
if (!p) {
|
||||
_queryInterval = const Duration(seconds: 6);
|
||||
}
|
||||
while (!_exit) {
|
||||
final now = DateTime.now();
|
||||
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
||||
@@ -276,7 +323,12 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
_queryOnlines(false);
|
||||
}
|
||||
} else {
|
||||
if (_queryCount < _maxQueryCount) {
|
||||
final skipIfIsWeb =
|
||||
isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage);
|
||||
final skipIfMobile =
|
||||
(isAndroid || isIOS) && !stateGlobal.isInMainPage;
|
||||
final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive;
|
||||
if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) {
|
||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
@@ -294,14 +346,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
_queryOnlines(bool isLoadEvent) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
_lastQueryPeers = {..._curPeers};
|
||||
if (isLoadEvent) {
|
||||
_lastChangeTime = DateTime.now();
|
||||
} else {
|
||||
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
||||
}
|
||||
_queryCount = 0;
|
||||
}
|
||||
_lastQueryPeers = {..._curPeers};
|
||||
if (isLoadEvent) {
|
||||
_lastChangeTime = DateTime.now();
|
||||
} else {
|
||||
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Peer>>? matchPeers(
|
||||
@@ -314,7 +366,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
if (!PeerSortType.values.contains(sortedBy)) {
|
||||
sortedBy = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: "peer-sorting",
|
||||
k: kOptionPeerSorting,
|
||||
v: sortedBy,
|
||||
);
|
||||
}
|
||||
@@ -357,28 +409,39 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
}
|
||||
|
||||
abstract class BasePeersView extends StatelessWidget {
|
||||
final String name;
|
||||
final String loadEvent;
|
||||
final PeerTabIndex peerTabIndex;
|
||||
final PeerFilter? peerFilter;
|
||||
final PeerCardBuilder peerCardBuilder;
|
||||
final GetInitPeers? getInitPeers;
|
||||
|
||||
const BasePeersView({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.loadEvent,
|
||||
required this.peerTabIndex,
|
||||
this.peerFilter,
|
||||
required this.peerCardBuilder,
|
||||
required this.getInitPeers,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Peers peers;
|
||||
switch (peerTabIndex) {
|
||||
case PeerTabIndex.recent:
|
||||
peers = gFFI.recentPeersModel;
|
||||
break;
|
||||
case PeerTabIndex.fav:
|
||||
peers = gFFI.favoritePeersModel;
|
||||
break;
|
||||
case PeerTabIndex.lan:
|
||||
peers = gFFI.lanPeersModel;
|
||||
break;
|
||||
case PeerTabIndex.ab:
|
||||
peers = gFFI.abModel.peersModel;
|
||||
break;
|
||||
case PeerTabIndex.group:
|
||||
peers = gFFI.groupModel.peersModel;
|
||||
break;
|
||||
}
|
||||
return _PeersView(
|
||||
peers:
|
||||
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
|
||||
peerFilter: peerFilter,
|
||||
peerCardBuilder: peerCardBuilder);
|
||||
peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,13 +450,11 @@ class RecentPeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'recent peer',
|
||||
loadEvent: LoadEvent.recent,
|
||||
peerTabIndex: PeerTabIndex.recent,
|
||||
peerCardBuilder: (Peer peer) => RecentPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -409,13 +470,11 @@ class FavoritePeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'favorite peer',
|
||||
loadEvent: LoadEvent.favorite,
|
||||
peerTabIndex: PeerTabIndex.fav,
|
||||
peerCardBuilder: (Peer peer) => FavoritePeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -431,13 +490,11 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'discovered peer',
|
||||
loadEvent: LoadEvent.lan,
|
||||
peerTabIndex: PeerTabIndex.lan,
|
||||
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -450,36 +507,38 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
|
||||
class AddressBookPeersView extends BasePeersView {
|
||||
AddressBookPeersView(
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required GetInitPeers getInitPeers})
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'address book peer',
|
||||
loadEvent: LoadEvent.addressBook,
|
||||
peerTabIndex: PeerTabIndex.ab,
|
||||
peerFilter: (Peer peer) =>
|
||||
_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
||||
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: getInitPeers,
|
||||
);
|
||||
|
||||
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||
if (selectedTags.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
// The result of a no-tag union with normal tags, still allows normal tags to perform union or intersection operations.
|
||||
final selectedNormalTags =
|
||||
selectedTags.where((tag) => tag != kUntagged).toList();
|
||||
if (selectedTags.contains(kUntagged)) {
|
||||
if (idents.isEmpty) return true;
|
||||
if (selectedNormalTags.isEmpty) return false;
|
||||
}
|
||||
if (gFFI.abModel.filterByIntersection.value) {
|
||||
for (final tag in selectedTags) {
|
||||
for (final tag in selectedNormalTags) {
|
||||
if (!idents.contains(tag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
for (final tag in selectedTags) {
|
||||
for (final tag in selectedNormalTags) {
|
||||
if (idents.contains(tag)) {
|
||||
return true;
|
||||
}
|
||||
@@ -491,20 +550,15 @@ class AddressBookPeersView extends BasePeersView {
|
||||
|
||||
class MyGroupPeerView extends BasePeersView {
|
||||
MyGroupPeerView(
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required GetInitPeers getInitPeers})
|
||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'group peer',
|
||||
loadEvent: LoadEvent.group,
|
||||
peerTabIndex: PeerTabIndex.group,
|
||||
peerFilter: filter,
|
||||
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
getInitPeers: getInitPeers,
|
||||
);
|
||||
|
||||
static bool filter(Peer peer) {
|
||||
|
||||
@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// https://github.com/flutter/flutter/issues/154053
|
||||
final useRawKeyEvents = isLinux && !isWeb;
|
||||
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
|
||||
// while `Alt` and `Control` are seperated key events for en-US input method.
|
||||
return FocusScope(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
canRequestFocus: true,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
onKey: (FocusNode data, RawKeyEvent e) =>
|
||||
inputModel.handleRawKeyEvent(e),
|
||||
onKey: useRawKeyEvents
|
||||
? (FocusNode data, RawKeyEvent event) =>
|
||||
inputModel.handleRawKeyEvent(event)
|
||||
: null,
|
||||
onKeyEvent: useRawKeyEvents
|
||||
? null
|
||||
: (FocusNode node, KeyEvent event) =>
|
||||
inputModel.handleKeyEvent(event),
|
||||
child: child));
|
||||
}
|
||||
}
|
||||
@@ -69,11 +79,22 @@ class RawTouchGestureDetectorRegion extends StatefulWidget {
|
||||
class _RawTouchGestureDetectorRegionState
|
||||
extends State<RawTouchGestureDetectorRegion> {
|
||||
Offset _cacheLongPressPosition = Offset(0, 0);
|
||||
// Timestamp of the last long press event.
|
||||
int _cacheLongPressPositionTs = 0;
|
||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||
double _scale = 1;
|
||||
|
||||
// Workaround tap down event when two fingers are used to scale(mobile)
|
||||
TapDownDetails? _lastTapDownDetails;
|
||||
|
||||
PointerDeviceKind? lastDeviceKind;
|
||||
|
||||
// For touch mode, onDoubleTap
|
||||
// `onDoubleTap()` does not provide the position of the tap event.
|
||||
Offset _lastPosOfDoubleTapDown = Offset.zero;
|
||||
bool _touchModePanStarted = false;
|
||||
Offset _doubleFinerTapPosition = Offset.zero;
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
InputModel get inputModel => widget.inputModel;
|
||||
@@ -88,142 +109,202 @@ class _RawTouchGestureDetectorRegionState
|
||||
);
|
||||
}
|
||||
|
||||
onTapDown(TapDownDetails d) {
|
||||
onTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
// Desktop or mobile "Touch mode"
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
inputModel.tapDown(MouseButtons.left);
|
||||
_lastTapDownDetails = d;
|
||||
}
|
||||
}
|
||||
|
||||
onTapUp(TapUpDetails d) {
|
||||
onTapUp(TapUpDetails d) async {
|
||||
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||
_lastTapDownDetails = null;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
inputModel.tapUp(MouseButtons.left);
|
||||
final isMoved =
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
if (isMoved) {
|
||||
if (lastTapDownDetails != null) {
|
||||
await inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
await inputModel.tapUp(MouseButtons.left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTap() {
|
||||
onTap() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
// Mobile, "Mouse mode"
|
||||
inputModel.tap(MouseButtons.left);
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleTapDown(TapDownDetails d) {
|
||||
onDoubleTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleTap() {
|
||||
onDoubleTap() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
inputModel.tap(MouseButtons.left);
|
||||
inputModel.tap(MouseButtons.left);
|
||||
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch &&
|
||||
!ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
|
||||
return;
|
||||
}
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
await inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
|
||||
onLongPressDown(LongPressDownDetails d) {
|
||||
onLongPressDown(LongPressDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
_lastPosOfDoubleTapDown = d.localPosition;
|
||||
_cacheLongPressPosition = d.localPosition;
|
||||
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
|
||||
return;
|
||||
}
|
||||
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressUp() {
|
||||
onLongPressUp() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
inputModel.tapUp(MouseButtons.left);
|
||||
await inputModel.tapUp(MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
// for mobiles
|
||||
onLongPress() {
|
||||
onLongPress() async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel
|
||||
final isMoved = await ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
if (!isMoved) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!ffi.ffiModel.isPeerMobile) {
|
||||
await inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
|
||||
onDoubleFinerTapDown(TapDownDetails d) {
|
||||
onDoubleFinerTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
_doubleFinerTapPosition = d.localPosition;
|
||||
// ignore for desktop and mobile
|
||||
}
|
||||
|
||||
onDoubleFinerTap(TapDownDetails d) {
|
||||
onDoubleFinerTap(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
|
||||
inputModel.tap(MouseButtons.right);
|
||||
|
||||
// mobile mouse mode or desktop touch screen
|
||||
final isMobileMouseMode = isMobile && !ffiModel.touchMode;
|
||||
// We can't use `d.localPosition` here because it's always (0, 0) on desktop.
|
||||
final isDesktopInRemoteRect = (isDesktop || isWebDesktop) &&
|
||||
ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition);
|
||||
if (isMobileMouseMode || isDesktopInRemoteRect) {
|
||||
await inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragStart(DragStartDetails d) {
|
||||
onHoldDragStart(DragStartDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
inputModel.sendMouse('down', MouseButtons.left);
|
||||
await inputModel.sendMouse('down', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragUpdate(DragUpdateDetails d) {
|
||||
onHoldDragUpdate(DragUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragEnd(DragEndDetails d) {
|
||||
onHoldDragEnd(DragEndDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onOneFingerPanStart(BuildContext context, DragStartDetails d) {
|
||||
onOneFingerPanStart(BuildContext context, DragStartDetails d) async {
|
||||
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||
_lastTapDownDetails = null;
|
||||
lastDeviceKind = d.kind ?? lastDeviceKind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
inputModel.sendMouse('down', MouseButtons.left);
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
if (lastTapDownDetails != null) {
|
||||
await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx,
|
||||
lastTapDownDetails.localPosition.dy);
|
||||
}
|
||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||
return;
|
||||
}
|
||||
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_touchModePanStarted = true;
|
||||
if (isDesktop || isWebDesktop) {
|
||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||
}
|
||||
|
||||
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
||||
// If the time interval between the start event and the first pan event is less than 500ms,
|
||||
// we consider to use the long press position as the start position.
|
||||
//
|
||||
// TODO: We should find a better way to send the first pan event as soon as possible.
|
||||
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
|
||||
500) {
|
||||
await ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
}
|
||||
await inputModel.sendMouse('down', MouseButtons.left);
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
} else {
|
||||
final offset = ffi.cursorModel.offset;
|
||||
final cursorX = offset.dx;
|
||||
@@ -232,33 +313,44 @@ class _RawTouchGestureDetectorRegionState
|
||||
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
|
||||
final size = MediaQueryData.fromView(View.of(context)).size;
|
||||
if (!visible.contains(Offset(cursorX, cursorY))) {
|
||||
ffi.cursorModel.move(size.width / 2, size.height / 2);
|
||||
await ffi.cursorModel.move(size.width / 2, size.height / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOneFingerPanUpdate(DragUpdateDetails d) {
|
||||
onOneFingerPanUpdate(DragUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch && !_touchModePanStarted) {
|
||||
return;
|
||||
}
|
||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
|
||||
onOneFingerPanEnd(DragEndDetails d) {
|
||||
onOneFingerPanEnd(DragEndDetails d) async {
|
||||
_touchModePanStarted = false;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
if (isDesktop || isWebDesktop) {
|
||||
ffi.cursorModel.clearRemoteWindowCoords();
|
||||
}
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
|
||||
// scale + pan event
|
||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||
_lastTapDownDetails = null;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
|
||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
@@ -267,7 +359,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
_scale = d.scale;
|
||||
|
||||
if (scale != 0) {
|
||||
bind.sessionSendPointer(
|
||||
await bind.sessionSendPointer(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(
|
||||
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
|
||||
@@ -275,28 +367,29 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
} else {
|
||||
// mobile
|
||||
ffi.canvasModel.updateScale(d.scale / _scale);
|
||||
ffi.canvasModel.updateScale(d.scale / _scale, d.focalPoint);
|
||||
_scale = d.scale;
|
||||
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
||||
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
||||
}
|
||||
}
|
||||
|
||||
onTwoFingerScaleEnd(ScaleEndDetails d) {
|
||||
onTwoFingerScaleEnd(ScaleEndDetails d) async {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
bind.sessionSendPointer(
|
||||
await bind.sessionSendPointer(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(
|
||||
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
|
||||
} else {
|
||||
// mobile
|
||||
_scale = 1;
|
||||
bind.sessionSetViewStyle(sessionId: sessionId, value: "");
|
||||
// No idea why we need to set the view style to "" here.
|
||||
// bind.sessionSetViewStyle(sessionId: sessionId, value: "");
|
||||
}
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
|
||||
get onHoldDragCancel => null;
|
||||
|
||||
@@ -3,15 +3,14 @@ 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';
|
||||
|
||||
customImageQualityWidget(
|
||||
{required double initQuality,
|
||||
required double initFps,
|
||||
required Function(double) setQuality,
|
||||
required Function(double) setFps,
|
||||
required Function(double)? setQuality,
|
||||
required Function(double)? setFps,
|
||||
required bool showFps,
|
||||
required bool showMoreQuality}) {
|
||||
if (initQuality < kMinQuality ||
|
||||
@@ -27,16 +26,12 @@ customImageQualityWidget(
|
||||
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
||||
final debouncerQuality = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setQuality(v);
|
||||
},
|
||||
onChanged: setQuality,
|
||||
initialValue: qualityValue.value,
|
||||
);
|
||||
final debouncerFps = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setFps(v);
|
||||
},
|
||||
onChanged: setFps,
|
||||
initialValue: fpsValue.value,
|
||||
);
|
||||
|
||||
@@ -62,10 +57,12 @@ customImageQualityWidget(
|
||||
divisions: moreQualityChecked.value
|
||||
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
||||
: ((kMaxQuality - kMinQuality) / 5).round(),
|
||||
onChanged: (double value) async {
|
||||
qualityValue.value = value;
|
||||
debouncerQuality.value = value;
|
||||
},
|
||||
onChanged: setQuality == null
|
||||
? null
|
||||
: (double value) async {
|
||||
qualityValue.value = value;
|
||||
debouncerQuality.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -124,10 +121,12 @@ customImageQualityWidget(
|
||||
min: kMinFps,
|
||||
max: kMaxFps,
|
||||
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
||||
onChanged: (double value) async {
|
||||
fpsValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
},
|
||||
onChanged: setFps == null
|
||||
? null
|
||||
: (double value) async {
|
||||
fpsValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -152,21 +151,29 @@ customImageQualitySetting() {
|
||||
final qualityKey = 'custom_image_quality';
|
||||
final fpsKey = 'custom-fps';
|
||||
|
||||
var initQuality =
|
||||
final initQuality =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
||||
kDefaultQuality);
|
||||
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
||||
kDefaultFps);
|
||||
final isQuanlityFixed = isOptionFixed(qualityKey);
|
||||
final initFps =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
||||
kDefaultFps);
|
||||
final isFpsFixed = isOptionFixed(fpsKey);
|
||||
|
||||
return customImageQualityWidget(
|
||||
initQuality: initQuality,
|
||||
initFps: initFps,
|
||||
setQuality: (v) {
|
||||
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
|
||||
},
|
||||
setFps: (v) {
|
||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||
},
|
||||
setQuality: isQuanlityFixed
|
||||
? null
|
||||
: (v) {
|
||||
bind.mainSetUserDefaultOption(
|
||||
key: qualityKey, value: v.toString());
|
||||
},
|
||||
setFps: isFpsFixed
|
||||
? null
|
||||
: (v) {
|
||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||
},
|
||||
showFps: true,
|
||||
showMoreQuality: true);
|
||||
}
|
||||
@@ -208,27 +215,31 @@ List<Widget> ServerConfigImportExportWidgets(
|
||||
|
||||
List<(String, String)> otherDefaultSettings() {
|
||||
List<(String, String)> v = [
|
||||
('View Mode', 'view_only'),
|
||||
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
|
||||
('Show remote cursor', 'show_remote_cursor'),
|
||||
if ((isDesktop || isWebDesktop)) ('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'),
|
||||
('View Mode', kOptionViewOnly),
|
||||
if ((isDesktop || isWebDesktop))
|
||||
('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||
if ((isDesktop || isWebDesktop))
|
||||
('Collapse toolbar', kOptionCollapseToolbar),
|
||||
('Show remote cursor', kOptionShowRemoteCursor),
|
||||
('Follow remote cursor', kOptionFollowRemoteCursor),
|
||||
('Follow remote window focus', kOptionFollowRemoteWindow),
|
||||
if ((isDesktop || isWebDesktop)) ('Zoom cursor', kOptionZoomCursor),
|
||||
('Show quality monitor', kOptionShowQualityMonitor),
|
||||
('Mute', kOptionDisableAudio),
|
||||
if (isDesktop) ('Enable file copy and paste', kOptionEnableFileCopyPaste),
|
||||
('Disable clipboard', kOptionDisableClipboard),
|
||||
('Lock after session end', kOptionLockAfterSessionEnd),
|
||||
('Privacy mode', kOptionPrivacyMode),
|
||||
if (isMobile) ('Touch mode', kOptionTouchMode),
|
||||
('True color (4:4:4)', kOptionI444),
|
||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||
('swap-left-right-mouse', 'swap-left-right-mouse'),
|
||||
if (isDesktop && useTextureRender)
|
||||
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||
if (isDesktop)
|
||||
(
|
||||
'Show displays as individual windows',
|
||||
kKeyShowDisplaysAsIndividualWindows
|
||||
),
|
||||
if (isDesktop && useTextureRender)
|
||||
if (isDesktop)
|
||||
(
|
||||
'Use all my displays for the remote session',
|
||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||
|
||||
@@ -6,9 +6,9 @@ import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.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;
|
||||
@@ -23,6 +23,20 @@ class TTextMenu {
|
||||
required this.onPressed,
|
||||
this.trailingIcon,
|
||||
this.divider = false});
|
||||
|
||||
Widget getChild() {
|
||||
if (trailingIcon != null) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
child,
|
||||
trailingIcon!,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TRadioMenu<T> {
|
||||
@@ -116,9 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// paste
|
||||
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Paste')),
|
||||
child: Text(translate('Send clipboard keystrokes')),
|
||||
onPressed: () async {
|
||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
@@ -133,12 +147,23 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
child: Text(translate('Reset canvas')),
|
||||
onPressed: () => ffi.cursorModel.reset()));
|
||||
}
|
||||
|
||||
connectWithToken(
|
||||
{required bool isFileTransfer, required bool isTcpTunneling}) {
|
||||
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
connToken: connToken);
|
||||
}
|
||||
|
||||
// transferFile
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer file')),
|
||||
onPressed: () => connect(context, id, isFileTransfer: true)),
|
||||
onPressed: () =>
|
||||
connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
|
||||
);
|
||||
}
|
||||
// tcpTunneling
|
||||
@@ -146,7 +171,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP tunneling')),
|
||||
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
||||
onPressed: () =>
|
||||
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
|
||||
);
|
||||
}
|
||||
// note
|
||||
@@ -169,7 +195,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text('${translate("Insert")} Ctrl + Alt + Del'),
|
||||
child: Text('${translate("Insert Ctrl + Alt + Del")}'),
|
||||
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
||||
);
|
||||
}
|
||||
@@ -327,7 +353,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
final alternativeCodecs =
|
||||
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
||||
final groupValue = await bind.sessionGetOption(
|
||||
sessionId: sessionId, arg: 'codec-preference') ??
|
||||
sessionId: sessionId, arg: kOptionCodecPreference) ??
|
||||
'';
|
||||
final List<bool> codecs = [];
|
||||
try {
|
||||
@@ -349,20 +375,25 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
onChanged(String? value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionPeerOption(
|
||||
sessionId: sessionId, name: 'codec-preference', value: value);
|
||||
sessionId: sessionId, name: kOptionCodecPreference, value: value);
|
||||
bind.sessionChangePreferCodec(sessionId: sessionId);
|
||||
}
|
||||
|
||||
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
||||
return TRadioMenu<String>(
|
||||
child: Text(translate(label)),
|
||||
child: Text(label),
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: enabled ? onChanged : null);
|
||||
}
|
||||
|
||||
var autoLabel = translate('Auto');
|
||||
if (groupValue == 'auto' &&
|
||||
ffi.qualityMonitorModel.data.codecFormat != null) {
|
||||
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
||||
}
|
||||
return [
|
||||
radio('Auto', 'auto', true),
|
||||
radio(autoLabel, 'auto', true),
|
||||
if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
||||
radio('VP9', 'vp9', true),
|
||||
if (codecs[1]) radio('AV1', 'av1', codecs[1]),
|
||||
@@ -371,12 +402,11 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
Future<List<TToggleMenu>> toolbarCursor(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
List<TToggleMenu> v = [];
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
|
||||
// show remote cursor
|
||||
@@ -384,12 +414,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland) {
|
||||
final state = ShowRemoteCursorState.find(id);
|
||||
final lockState = ShowRemoteCursorLockState.find(id);
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'show-remote-cursor';
|
||||
if (pi.currentDisplay == kAllDisplayValue ||
|
||||
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
lockState.value = false;
|
||||
}
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Show remote cursor')),
|
||||
value: state.value,
|
||||
onChanged: enabled
|
||||
onChanged: enabled && !lockState.value
|
||||
? (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
@@ -399,6 +434,67 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
}
|
||||
: null));
|
||||
}
|
||||
// follow remote cursor
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland &&
|
||||
versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||
pi.displays.length > 1 &&
|
||||
pi.currentDisplay != kAllDisplayValue &&
|
||||
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
final option = 'follow-remote-cursor';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
final showCursorOption = 'show-remote-cursor';
|
||||
final showCursorState = ShowRemoteCursorState.find(id);
|
||||
final showCursorLockState = ShowRemoteCursorLockState.find(id);
|
||||
final showCursorEnabled = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
showCursorLockState.value = value;
|
||||
if (value && !showCursorEnabled) {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: showCursorOption);
|
||||
showCursorState.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
}
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Follow remote cursor')),
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: option);
|
||||
showCursorLockState.value = value;
|
||||
if (!showCursorEnabled) {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: showCursorOption);
|
||||
showCursorState.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
}
|
||||
}));
|
||||
}
|
||||
// follow remote window focus
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland &&
|
||||
versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||
pi.displays.length > 1 &&
|
||||
pi.currentDisplay != kAllDisplayValue &&
|
||||
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
final option = 'follow-remote-window';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Follow remote window focus')),
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: option);
|
||||
}));
|
||||
}
|
||||
// zoom cursor
|
||||
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
|
||||
if (!isMobile &&
|
||||
@@ -417,6 +513,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
},
|
||||
));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
List<TToggleMenu> v = [];
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
|
||||
// show quality monitor
|
||||
final option = 'show-quality-monitor';
|
||||
v.add(TToggleMenu(
|
||||
@@ -441,20 +548,27 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Mute'))));
|
||||
}
|
||||
// file copy and paste
|
||||
// If the version is less than 1.2.4, file copy and paste is supported on Windows only.
|
||||
final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
|
||||
isWindows &&
|
||||
pi.platform == kPeerPlatformWindows;
|
||||
// If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
|
||||
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
|
||||
if (ffiModel.keyboard &&
|
||||
perms['file'] != false &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
|
||||
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'enable-file-transfer';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
final value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: kOptionEnableFileCopyPaste);
|
||||
}
|
||||
: null,
|
||||
child: Text(translate('Enable file copy and paste'))));
|
||||
@@ -477,7 +591,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Disable clipboard'))));
|
||||
}
|
||||
// lock after session end
|
||||
if (ffiModel.keyboard) {
|
||||
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'lock-after-session-end';
|
||||
final value =
|
||||
@@ -493,8 +607,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Lock after session end'))));
|
||||
}
|
||||
|
||||
if (useTextureRender &&
|
||||
pi.isSupportMultiDisplay &&
|
||||
if (pi.isSupportMultiDisplay &&
|
||||
PrivacyModeState.find(id).isEmpty &&
|
||||
pi.displaysCount.value > 1 &&
|
||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||
@@ -506,13 +619,13 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionSetDisplaysAsIndividualWindows(
|
||||
sessionId: sessionId, value: value ? 'Y' : '');
|
||||
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||
},
|
||||
child: Text(translate('Show displays as individual windows'))));
|
||||
}
|
||||
|
||||
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
||||
if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) {
|
||||
if (pi.isSupportMultiDisplay && isMultiScreens) {
|
||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||
sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
@@ -521,7 +634,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
|
||||
sessionId: sessionId, value: value ? 'Y' : '');
|
||||
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||
},
|
||||
child: Text(translate('Use all my displays for the remote session'))));
|
||||
}
|
||||
@@ -547,6 +660,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
v.addAll(toolbarKeyboardToggles(ffi));
|
||||
}
|
||||
|
||||
// view mode (mobile only, desktop is in keyboard menu)
|
||||
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
|
||||
v.add(TToggleMenu(
|
||||
value: ffiModel.viewOnly,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, value);
|
||||
},
|
||||
child: Text(translate('View Mode'))));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -687,3 +812,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
bool showVirtualDisplayMenu(FFI ffi) {
|
||||
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||
return false;
|
||||
}
|
||||
if (!ffi.ffiModel.pi.isInstalled) {
|
||||
return false;
|
||||
}
|
||||
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Widget> getVirtualDisplayMenuChildren(
|
||||
FFI ffi, String id, VoidCallback? clickCallBack) {
|
||||
if (!showVirtualDisplayMenu(ffi)) {
|
||||
return [];
|
||||
}
|
||||
final pi = ffi.ffiModel.pi;
|
||||
final privacyModeState = PrivacyModeState.find(id);
|
||||
if (pi.isRustDeskIdd) {
|
||||
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
||||
children.add(Obx(() => CkbMenuButton(
|
||||
value: virtualDisplays.contains(i + 1),
|
||||
onChanged: privacyModeState.isNotEmpty
|
||||
? null
|
||||
: (bool? value) async {
|
||||
if (value != null) {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId, index: i + 1, on: value);
|
||||
clickCallBack?.call();
|
||||
}
|
||||
},
|
||||
child: Text('${translate('Virtual display')} ${i + 1}'),
|
||||
ffi: ffi,
|
||||
)));
|
||||
}
|
||||
children.add(Divider());
|
||||
children.add(Obx(() => MenuButton(
|
||||
onPressed: privacyModeState.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false);
|
||||
clickCallBack?.call();
|
||||
},
|
||||
ffi: ffi,
|
||||
child: Text(translate('Plug out all')),
|
||||
)));
|
||||
return children;
|
||||
}
|
||||
if (pi.isAmyuniIdd) {
|
||||
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
||||
final children = <Widget>[
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId, index: 0, on: false);
|
||||
clickCallBack?.call();
|
||||
},
|
||||
child: Icon(Icons.remove),
|
||||
),
|
||||
Text(count.toString()),
|
||||
TextButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 4
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId, index: 0, on: true);
|
||||
clickCallBack?.call();
|
||||
},
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
)),
|
||||
Divider(),
|
||||
Obx(() => MenuButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false);
|
||||
clickCallBack?.call();
|
||||
},
|
||||
ffi: ffi,
|
||||
child: Text(translate('Plug out all')),
|
||||
)),
|
||||
];
|
||||
return children;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ const kKeyTranslateMode = 'translate';
|
||||
const String kPlatformAdditionsIsWayland = "is_wayland";
|
||||
const String kPlatformAdditionsHeadless = "headless";
|
||||
const String kPlatformAdditionsIsInstalled = "is_installed";
|
||||
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
|
||||
const String kPlatformAdditionsIddImpl = "idd_impl";
|
||||
const String kPlatformAdditionsRustDeskVirtualDisplays =
|
||||
"rustdesk_virtual_displays";
|
||||
const String kPlatformAdditionsAmyuniVirtualDisplays =
|
||||
"amyuni_virtual_displays";
|
||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||
"supported_privacy_mode_impl";
|
||||
@@ -28,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
const String kPeerPlatformMacOS = "Mac OS";
|
||||
const String kPeerPlatformAndroid = "Android";
|
||||
const String kPeerPlatformWebDesktop = "WebDesktop";
|
||||
|
||||
const double kScrollbarThickness = 12.0;
|
||||
|
||||
@@ -58,16 +63,100 @@ 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 kWindowEventRemoteWindowCoords = "remote_window_coords";
|
||||
const String kWindowEventSetFullscreen = "set_fullscreen";
|
||||
|
||||
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
||||
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
||||
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||
|
||||
const String kOptionViewStyle = "view_style";
|
||||
const String kOptionScrollStyle = "scroll_style";
|
||||
const String kOptionImageQuality = "image_quality";
|
||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionTextureRender = "use-texture-render";
|
||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||
const String kOptionOpenInWindows = "allow-open-in-windows";
|
||||
const String kOptionForceAlwaysRelay = "force-always-relay";
|
||||
const String kOptionViewOnly = "view-only";
|
||||
const String kOptionViewOnly = "view_only";
|
||||
const String kOptionEnableLanDiscovery = "enable-lan-discovery";
|
||||
const String kOptionWhitelist = "whitelist";
|
||||
const String kOptionEnableAbr = "enable-abr";
|
||||
const String kOptionEnableRecordSession = "enable-record-session";
|
||||
const String kOptionDirectServer = "direct-server";
|
||||
const String kOptionDirectAccessPort = "direct-access-port";
|
||||
const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
|
||||
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
||||
const String kOptionEnableHwcodec = "enable-hwcodec";
|
||||
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
||||
const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing";
|
||||
const String kOptionVideoSaveDirectory = "video-save-directory";
|
||||
const String kOptionAccessMode = "access-mode";
|
||||
const String kOptionEnableKeyboard = "enable-keyboard";
|
||||
// "Settings -> Security -> Permissions"
|
||||
const String kOptionEnableClipboard = "enable-clipboard";
|
||||
const String kOptionEnableFileTransfer = "enable-file-transfer";
|
||||
const String kOptionEnableAudio = "enable-audio";
|
||||
const String kOptionEnableTunnel = "enable-tunnel";
|
||||
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||
const String kOptionEnableBlockInput = "enable-block-input";
|
||||
const String kOptionAllowRemoteConfigModification =
|
||||
"allow-remote-config-modification";
|
||||
const String kOptionVerificationMethod = "verification-method";
|
||||
const String kOptionApproveMode = "approve-mode";
|
||||
const String kOptionCollapseToolbar = "collapse_toolbar";
|
||||
const String kOptionShowRemoteCursor = "show_remote_cursor";
|
||||
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
|
||||
const String kOptionFollowRemoteWindow = "follow_remote_window";
|
||||
const String kOptionZoomCursor = "zoom-cursor";
|
||||
const String kOptionShowQualityMonitor = "show_quality_monitor";
|
||||
const String kOptionDisableAudio = "disable_audio";
|
||||
const String kOptionEnableFileCopyPaste = "enable-file-copy-paste";
|
||||
// "Settings -> Display -> Other default options"
|
||||
const String kOptionDisableClipboard = "disable_clipboard";
|
||||
const String kOptionLockAfterSessionEnd = "lock_after_session_end";
|
||||
const String kOptionPrivacyMode = "privacy_mode";
|
||||
const String kOptionTouchMode = "touch-mode";
|
||||
const String kOptionI444 = "i444";
|
||||
const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
|
||||
const String kOptionCodecPreference = "codec-preference";
|
||||
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
|
||||
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
|
||||
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
|
||||
const String kOptionRemoteMenubarState = "remoteMenubarState";
|
||||
const String kOptionPeerSorting = "peer-sorting";
|
||||
const String kOptionPeerTabIndex = "peer-tab-index";
|
||||
const String kOptionPeerTabOrder = "peer-tab-order";
|
||||
const String kOptionPeerTabVisible = "peer-tab-visible";
|
||||
const String kOptionPeerCardUiType = "peer-card-ui-type";
|
||||
const String kOptionCurrentAbName = "current-ab-name";
|
||||
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
||||
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
||||
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||
const String kOptionStopService = "stop-service";
|
||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||
|
||||
// buildin opitons
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||
const String kOptionRemovePresetPasswordWarning =
|
||||
"remove-preset-password-warning";
|
||||
const kHideUsernameOnCard = "hide-username-on-card";
|
||||
const String kOptionHideHelpCards = "hide-help-cards";
|
||||
|
||||
const String kOptionToggleViewOnly = "view-only";
|
||||
|
||||
const String kOptionDisableFloatingWindow = "disable-floating-window";
|
||||
|
||||
const String kOptionKeepScreenOn = "keep-screen-on";
|
||||
|
||||
const String kOptionShowMobileAction = "showMobileActions";
|
||||
|
||||
const String kUrlActionClose = "close";
|
||||
|
||||
@@ -80,6 +169,15 @@ const int kWindowMainId = 0;
|
||||
const String kPointerEventKindTouch = "touch";
|
||||
const String kPointerEventKindMouse = "mouse";
|
||||
|
||||
const String kMouseEventTypeDefault = "";
|
||||
const String kMouseEventTypePanStart = "pan_start";
|
||||
const String kMouseEventTypePanUpdate = "pan_update";
|
||||
const String kMouseEventTypePanEnd = "pan_end";
|
||||
const String kMouseEventTypeDown = "down";
|
||||
const String kMouseEventTypeUp = "up";
|
||||
|
||||
const String kKeyFlutterKey = "flutter_key";
|
||||
|
||||
const String kKeyShowDisplaysAsIndividualWindows =
|
||||
'displays_as_individual_windows';
|
||||
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||
@@ -87,10 +185,13 @@ const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
||||
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
||||
|
||||
const String kMsgboxTextWaitingForImage = 'Connected, waiting for image...';
|
||||
|
||||
// the executable name of the portable version
|
||||
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
||||
|
||||
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
|
||||
const Color kColorCanvas = Colors.black;
|
||||
|
||||
const int kMobileDefaultDisplayWidth = 720;
|
||||
const int kMobileDefaultDisplayHeight = 1280;
|
||||
@@ -121,12 +222,11 @@ double kNewWindowOffset = isWindows
|
||||
? 30.0
|
||||
: 50.0;
|
||||
|
||||
EdgeInsets get kDragToResizeAreaPadding =>
|
||||
!kUseCompatibleUiMode && isLinux
|
||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(5.0)
|
||||
: EdgeInsets.zero;
|
||||
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
|
||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(5.0)
|
||||
: EdgeInsets.zero;
|
||||
// https://en.wikipedia.org/wiki/Non-breaking_space
|
||||
const int $nbsp = 0x00A0;
|
||||
|
||||
@@ -144,15 +244,17 @@ const double kDesktopIconButtonSplashRadius = 20;
|
||||
/// [kMinCursorSize] indicates min cursor (w, h)
|
||||
const int kMinCursorSize = 12;
|
||||
|
||||
/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse
|
||||
const kDefaultScrollAmountMultiplier = 5.0;
|
||||
const kDefaultScrollDuration = Duration(milliseconds: 50);
|
||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||
const kFullScreenEdgeSize = 0.0;
|
||||
const kMaximizeEdgeSize = 0.0;
|
||||
var kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
|
||||
const kWindowResizeEdgeSize = 5.0;
|
||||
const kWindowBorderWidth = 1.0;
|
||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||
const kFrameBorderRadius = 12.0;
|
||||
const kFrameClipRRectBorderRadius = 12.0;
|
||||
const kFrameBoxShadowBlurRadius = 32.0;
|
||||
const kFrameBoxShadowOffsetFocused = 4.0;
|
||||
const kFrameBoxShadowOffsetUnfocused = 2.0;
|
||||
|
||||
const kInvalidValueStr = 'InvalidValueStr';
|
||||
|
||||
@@ -198,12 +300,6 @@ const kRemoteImageQualityLow = 'low';
|
||||
/// [kRemoteImageQualityCustom] Custom image quality.
|
||||
const kRemoteImageQualityCustom = 'custom';
|
||||
|
||||
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
|
||||
const kRemoteAudioGuestToHost = 'guest-to-host';
|
||||
|
||||
/// [kRemoteAudioDualWay] dual-way audio mode(default).
|
||||
const kRemoteAudioDualWay = 'dual-way';
|
||||
|
||||
const kIgnoreDpi = true;
|
||||
|
||||
// ================================ mobile ================================
|
||||
@@ -222,6 +318,7 @@ const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
|
||||
const kRequestIgnoreBatteryOptimizations =
|
||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
||||
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
||||
const kAndroid13Notification = "android.permission.POST_NOTIFICATIONS";
|
||||
|
||||
/// Android channel invoke type key
|
||||
class AndroidChannel {
|
||||
@@ -477,3 +574,5 @@ enum WindowsTarget {
|
||||
extension WindowsTargetExt on int {
|
||||
WindowsTarget get windowsVersion => getWindowsTarget(this);
|
||||
}
|
||||
|
||||
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
|
||||
@@ -3,8 +3,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -169,16 +169,12 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
|
||||
final status =
|
||||
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
||||
final statusNum = status['status_num'] as int;
|
||||
final preStatus = stateGlobal.svcStatus.value;
|
||||
if (statusNum == 0) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
||||
} else if (statusNum == -1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
} else if (statusNum == 1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.ready;
|
||||
if (preStatus != SvcStatus.ready) {
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
}
|
||||
} else {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
}
|
||||
@@ -207,19 +203,21 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
// https://github.com/flutter/flutter/issues/157244
|
||||
Iterable<Peer> _autocompleteOpts = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
if (lastRemoteId != _idController.id) {
|
||||
setState(() {
|
||||
_idController.id = lastRemoteId;
|
||||
});
|
||||
}
|
||||
}();
|
||||
});
|
||||
}
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
windowManager.addListener(this);
|
||||
@@ -261,8 +259,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
@override
|
||||
void onWindowLeaveFullScreen() {
|
||||
// Restore edge border to default edge size.
|
||||
stateGlobal.resizeEdgeSize.value =
|
||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
|
||||
stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
|
||||
? kMaximizeEdgeSize
|
||||
: windowResizeEdgeSize;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -326,43 +325,14 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
child: Ink(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
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),
|
||||
getConnectionPageTitle(context, false).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Autocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.empty();
|
||||
_autocompleteOpts = const Iterable<Peer>.empty();
|
||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
@@ -378,7 +348,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
);
|
||||
return [emptyPeer];
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
String textWithoutSpaces =
|
||||
textEditingValue.text.replaceAll(" ", "");
|
||||
@@ -389,8 +359,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
);
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
|
||||
return peers
|
||||
_autocompleteOpts = peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
@@ -402,6 +371,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
peer.alias.toLowerCase().contains(textToFind))
|
||||
.toList();
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
BuildContext context,
|
||||
@@ -462,6 +432,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
optionsViewBuilder: (BuildContext context,
|
||||
AutocompleteOnSelected<Peer> onSelected,
|
||||
Iterable<Peer> options) {
|
||||
options = _autocompleteOpts;
|
||||
double maxHeight = options.length * 50;
|
||||
if (options.length == 1) {
|
||||
maxHeight = 52;
|
||||
|
||||
@@ -12,9 +12,9 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -40,7 +40,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
var updateUrl = '';
|
||||
var systemError = '';
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
var svcStopped = false.obs;
|
||||
@@ -69,44 +68,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildPresetPasswordWarning() {
|
||||
return FutureBuilder<bool>(
|
||||
future: bind.isPresetPassword(),
|
||||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(
|
||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||
} else if (snapshot.hasData && snapshot.data == true) {
|
||||
return Container(
|
||||
color: Colors.yellow,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
child: Text(
|
||||
translate("Security Alert"),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
translate("preset_password_warning"),
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
); // Show a warning message if the Future completed with true
|
||||
} else {
|
||||
return SizedBox
|
||||
.shrink(); // Show nothing if the Future completed with false or null
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLeftPane(BuildContext context) {
|
||||
final isIncomingOnly = bind.isIncomingOnly();
|
||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||
@@ -115,22 +76,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
if (bind.isCustomClient())
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://rustdesk.com'));
|
||||
},
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Text(
|
||||
translate("powered_by_me"),
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 9, decoration: TextDecoration.underline),
|
||||
)),
|
||||
),
|
||||
).marginOnly(top: 6),
|
||||
child: loadPowered(context),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
@@ -140,7 +86,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
if (!isOutgoingOnly) buildIDBoard(context),
|
||||
if (!isOutgoingOnly) buildPasswordBoard(context),
|
||||
FutureBuilder<Widget>(
|
||||
future: buildHelpCards(),
|
||||
future: Future.value(
|
||||
Obx(() => buildHelpCards(stateGlobal.updateUrl.value))),
|
||||
builder: (_, data) {
|
||||
if (data.hasData) {
|
||||
if (isIncomingOnly) {
|
||||
@@ -178,41 +125,43 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: Container(
|
||||
width: isIncomingOnly ? 280.0 : 200.0,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: DesktopScrollWrapper(
|
||||
scrollController: _leftPaneScrollController,
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
controller: _leftPaneScrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
key: _childKey,
|
||||
children: children,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
controller: _leftPaneScrollController,
|
||||
child: Column(
|
||||
key: _childKey,
|
||||
children: children,
|
||||
),
|
||||
if (isOutgoingOnly)
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: 12,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: InkWell(
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.settings,
|
||||
color: _editHover.value
|
||||
? textColor
|
||||
: Colors.grey.withOpacity(0.5),
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
if (isOutgoingOnly)
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: 12,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: InkWell(
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.settings,
|
||||
color: _editHover.value
|
||||
? textColor
|
||||
: Colors.grey.withOpacity(0.5),
|
||||
size: 22,
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(0),
|
||||
onHover: (value) => _editHover.value = value,
|
||||
),
|
||||
onTap: () => {
|
||||
if (DesktopSettingPage.tabKeys.isNotEmpty)
|
||||
{
|
||||
DesktopSettingPage.switch2page(
|
||||
DesktopSettingPage.tabKeys[0])
|
||||
}
|
||||
},
|
||||
onHover: (value) => _editHover.value = value,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -298,19 +247,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
RxBool hover = false.obs;
|
||||
return InkWell(
|
||||
onTap: DesktopTabPage.onAddSetting,
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? 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),
|
||||
)),
|
||||
child: Tooltip(
|
||||
message: translate('Settings'),
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onHover: (value) => hover.value = value,
|
||||
@@ -318,10 +268,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
|
||||
buildPasswordBoard(BuildContext context) {
|
||||
final model = gFFI.serverModel;
|
||||
return ChangeNotifierProvider.value(
|
||||
value: gFFI.serverModel,
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, model, child) {
|
||||
return buildPasswordBoard2(context, model);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
buildPasswordBoard2(BuildContext context, ServerModel model) {
|
||||
RxBool refreshHover = false.obs;
|
||||
RxBool editHover = false.obs;
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
final showOneTime = model.approveMode != 'click' &&
|
||||
model.verificationMethod != kUsePermanentPassword;
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
|
||||
child: Row(
|
||||
@@ -350,8 +311,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () {
|
||||
if (model.verificationMethod !=
|
||||
kUsePermanentPassword) {
|
||||
if (showOneTime) {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: model.serverPasswd.text));
|
||||
showToast(translate("Copied"));
|
||||
@@ -369,35 +329,39 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedRotationWidget(
|
||||
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
if (showOneTime)
|
||||
AnimatedRotationWidget(
|
||||
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)))),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
))),
|
||||
),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
if (!bind.isDisableSettings())
|
||||
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),
|
||||
child: Tooltip(
|
||||
message: translate('Change Password'),
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.edit,
|
||||
color: editHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
),
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(0),
|
||||
onTap: () => DesktopSettingPage.switch2page(
|
||||
SettingsTabKey.safety),
|
||||
onHover: (value) => editHover.value = value,
|
||||
),
|
||||
],
|
||||
@@ -452,7 +416,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
);
|
||||
}
|
||||
|
||||
Future<Widget> buildHelpCards() async {
|
||||
Widget buildHelpCards(String updateUrl) {
|
||||
if (!bind.isCustomClient() &&
|
||||
updateUrl.isNotEmpty &&
|
||||
!isCardClosed &&
|
||||
@@ -486,14 +450,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
});
|
||||
}
|
||||
} else if (isMacOS) {
|
||||
if (!(bind.isOutgoingOnly() ||
|
||||
bind.mainIsCanScreenRecording(prompt: false))) {
|
||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||
if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
|
||||
return buildInstallCard("Permissions", "config_screen", "Configure",
|
||||
() async {
|
||||
bind.mainIsCanScreenRecording(prompt: true);
|
||||
watchIsCanScreenRecording = true;
|
||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||
} else if (!bind.mainIsProcessTrusted(prompt: false)) {
|
||||
} else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
|
||||
return buildInstallCard("Permissions", "config_acc", "Configure",
|
||||
() async {
|
||||
bind.mainIsProcessTrusted(prompt: true);
|
||||
@@ -505,7 +469,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
bind.mainIsCanInputMonitoring(prompt: true);
|
||||
watchIsInputMonitoring = true;
|
||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||
} else if (!svcStopped.value &&
|
||||
} else if (!isOutgoingOnly &&
|
||||
!svcStopped.value &&
|
||||
bind.mainIsInstalled() &&
|
||||
!bind.mainIsInstalledDaemon(prompt: false)) {
|
||||
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
||||
@@ -588,6 +553,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
String? link,
|
||||
bool? closeButton,
|
||||
String? closeOption}) {
|
||||
if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
|
||||
content != 'install_daemon_tip') {
|
||||
return const SizedBox();
|
||||
}
|
||||
void closeCard() async {
|
||||
if (closeOption != null) {
|
||||
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
||||
@@ -701,10 +670,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer(const Duration(seconds: 1), () async {
|
||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||
if (updateUrl.isNotEmpty) setState(() {});
|
||||
});
|
||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||
await gFFI.serverModel.fetchID();
|
||||
final error = await bind.mainGetError();
|
||||
@@ -712,7 +677,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
systemError = error;
|
||||
setState(() {});
|
||||
}
|
||||
final v = await bind.mainGetOption(key: "stop-service") == "Y";
|
||||
final v = await mainGetBoolOption(kOptionStopService);
|
||||
if (v != svcStopped.value) {
|
||||
svcStopped.value = v;
|
||||
setState(() {});
|
||||
@@ -802,6 +767,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
isRDP: call.arguments['isRDP'],
|
||||
password: call.arguments['password'],
|
||||
forceRelay: call.arguments['forceRelay'],
|
||||
connToken: call.arguments['connToken'],
|
||||
);
|
||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||
final args = call.arguments.split(',');
|
||||
@@ -824,6 +790,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
await rustDeskWinManager.openMonitorSession(
|
||||
windowId, peerId, display, displayCount, screenRect);
|
||||
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||
final windowId = int.tryParse(call.arguments);
|
||||
if (windowId != null) {
|
||||
return jsonEncode(
|
||||
await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
|
||||
}
|
||||
}
|
||||
});
|
||||
_uniLinksSubscription = listenUniLinks();
|
||||
@@ -854,6 +826,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
_uniLinksSubscription?.cancel();
|
||||
Get.delete<RxBool>(tag: 'stop-service');
|
||||
_updateTimer?.cancel();
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.unregisterEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -873,7 +849,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
}
|
||||
|
||||
void setPasswordDialog() async {
|
||||
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
final p0 = TextEditingController(text: pw);
|
||||
final p1 = TextEditingController(text: pw);
|
||||
@@ -887,6 +863,7 @@ void setPasswordDialog() async {
|
||||
// SpecialCharacterValidationRule(),
|
||||
MinCharactersValidationRule(8),
|
||||
];
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
@@ -913,6 +890,9 @@ void setPasswordDialog() async {
|
||||
return;
|
||||
}
|
||||
bind.mainSetPermanentPassword(password: pass);
|
||||
if (pass.isNotEmpty) {
|
||||
notEmptyCallback?.call();
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -942,6 +922,7 @@ void setPasswordDialog() async {
|
||||
errMsg0 = '';
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -968,6 +949,7 @@ void setPasswordDialog() async {
|
||||
errMsg1 = '';
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
|
||||
import '../../common/shared_state.dart';
|
||||
|
||||
@@ -17,9 +18,10 @@ class DesktopTabPage extends StatefulWidget {
|
||||
@override
|
||||
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
||||
|
||||
static void onAddSetting({int initialPage = 0}) {
|
||||
static void onAddSetting(
|
||||
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
||||
try {
|
||||
DesktopTabController tabController = Get.find();
|
||||
DesktopTabController tabController = Get.find<DesktopTabController>();
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelSettingPage,
|
||||
label: kTabLabelSettingPage,
|
||||
@@ -27,7 +29,7 @@ class DesktopTabPage extends StatefulWidget {
|
||||
unselectedIcon: Icons.build_outlined,
|
||||
page: DesktopSettingPage(
|
||||
key: const ValueKey(kTabLabelSettingPage),
|
||||
initialPage: initialPage,
|
||||
initialTabkey: initialPage,
|
||||
)));
|
||||
} catch (e) {
|
||||
debugPrintStack(label: '$e');
|
||||
@@ -35,14 +37,16 @@ class DesktopTabPage extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
class _DesktopTabPageState extends State<DesktopTabPage>
|
||||
with WidgetsBindingObserver {
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
final RxBool _block = false.obs;
|
||||
// bool mouseIn = false;
|
||||
|
||||
_DesktopTabPageState() {
|
||||
RemoteCountState.init();
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelHomePage,
|
||||
label: kTabLabelHomePage,
|
||||
@@ -56,19 +60,47 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
tabController.onSelected = (key) {
|
||||
if (key == kTabLabelHomePage) {
|
||||
windowManager.setSize(getIncomingOnlyHomeSize());
|
||||
windowManager.setResizable(false);
|
||||
setResizable(false);
|
||||
} else {
|
||||
windowManager.setSize(getIncomingOnlySettingsSize());
|
||||
windowManager.setResizable(true);
|
||||
setResizable(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
shouldBeBlocked(_block, canBeBlocked);
|
||||
} else if (state == AppLifecycleState.inactive) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
/*
|
||||
bool _handleKeyEvent(KeyEvent event) {
|
||||
if (!mouseIn && event is KeyDownEvent) {
|
||||
print('key down: ${event.logicalKey}');
|
||||
shouldBeBlocked(_block, canBeBlocked);
|
||||
}
|
||||
return false; // allow it to propagate
|
||||
}
|
||||
*/
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
Get.delete<DesktopTabController>();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -87,12 +119,14 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
isClose: false,
|
||||
),
|
||||
),
|
||||
blockTab: _block,
|
||||
)));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(
|
||||
() => DragToResizeArea(
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||
child: tabWidget,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
@@ -16,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_hbb/web/dummy.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
@@ -54,21 +57,23 @@ class FileManagerPage extends StatefulWidget {
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.isSharedPassword,
|
||||
required this.tabController,
|
||||
this.tabController,
|
||||
this.connToken,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
final DesktopTabController tabController;
|
||||
final String? connToken;
|
||||
final DesktopTabController? tabController;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
}
|
||||
|
||||
class _FileManagerPageState extends State<FileManagerPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
||||
|
||||
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
||||
@@ -87,18 +92,26 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
isFileTransfer: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
connToken: widget.connToken,
|
||||
forceRelay: widget.forceRelay);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||
Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
if (isWeb) {
|
||||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||
}
|
||||
debugPrint("File manager page init success with id ${widget.id}");
|
||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -111,12 +124,21 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||
});
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
jobController.jobTable.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@@ -126,10 +148,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
model.localController, _ffi, _mouseFocusScope))),
|
||||
if (!isWeb)
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
model.localController, _ffi, _mouseFocusScope))),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: dropArea(FileManagerView(
|
||||
@@ -170,10 +193,31 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
/// transfer status list
|
||||
/// watch transfer status
|
||||
Widget statusList() {
|
||||
Widget getIcon(JobProgress job) {
|
||||
final color = Theme.of(context).tabBarTheme.labelColor;
|
||||
switch (job.type) {
|
||||
case JobType.deleteDir:
|
||||
case JobType.deleteFile:
|
||||
return Icon(Icons.delete_outline, color: color);
|
||||
default:
|
||||
return Transform.rotate(
|
||||
angle: isWeb
|
||||
? job.isRemoteToLocal
|
||||
? pi / 2
|
||||
: pi / 2 * 3
|
||||
: job.isRemoteToLocal
|
||||
? pi
|
||||
: 0,
|
||||
child: Icon(Icons.arrow_forward_ios, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
statusListView(List<JobProgress> jobs) => ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = jobs[index];
|
||||
final status = item.getStatus();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
child: generateCard(
|
||||
@@ -183,15 +227,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemoteToLocal ? pi : 0,
|
||||
child: SvgPicture.asset("assets/arrow.svg",
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context).tabBarTheme.labelColor)),
|
||||
).paddingOnly(left: 15),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
getIcon(item)
|
||||
.marginSymmetric(horizontal: 10, vertical: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -200,45 +237,28 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: item.jobName,
|
||||
child: Text(
|
||||
item.fileName,
|
||||
child: ExtendedText(
|
||||
item.jobName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
Text(
|
||||
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
overflowWidget: TextOverflowWidget(
|
||||
child: Text("..."),
|
||||
position: TextOverflowPosition.start),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state != JobState.inProgress,
|
||||
child: Text(
|
||||
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: status,
|
||||
child: Text(status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
)).marginOnly(top: 6),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state == JobState.inProgress,
|
||||
child: Text(
|
||||
translate(
|
||||
item.display(),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state != JobState.inProgress,
|
||||
offstage: item.type != JobType.transfer ||
|
||||
item.state != JobState.inProgress,
|
||||
child: LinearPercentIndicator(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
animateFromLastPercent: true,
|
||||
center: Text(
|
||||
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
||||
@@ -248,7 +268,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
progressColor: MyTheme.accent,
|
||||
backgroundColor: Theme.of(context).hoverColor,
|
||||
lineHeight: kDesktopFileTransferRowHeight,
|
||||
).paddingSymmetric(vertical: 15),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -259,6 +279,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Offstage(
|
||||
offstage: item.state != JobState.paused,
|
||||
child: MenuButton(
|
||||
tooltip: translate("Resume"),
|
||||
onPressed: () {
|
||||
jobController.resumeJob(item.id);
|
||||
},
|
||||
@@ -271,7 +292,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
),
|
||||
),
|
||||
MenuButton(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
tooltip: translate("Delete"),
|
||||
child: SvgPicture.asset(
|
||||
"assets/close.svg",
|
||||
colorFilter: svgColor(Colors.white),
|
||||
@@ -284,11 +305,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
hoverColor: MyTheme.accent80,
|
||||
),
|
||||
],
|
||||
),
|
||||
).marginAll(12),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -472,6 +493,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
}
|
||||
|
||||
Widget headTools() {
|
||||
var uploadButtonTapPosition = RelativeRect.fill;
|
||||
RxBool isUploadFolder =
|
||||
(bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
|
||||
return Container(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -518,6 +542,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
Row(
|
||||
children: [
|
||||
MenuButton(
|
||||
tooltip: translate('Back'),
|
||||
padding: EdgeInsets.only(
|
||||
right: 3,
|
||||
),
|
||||
@@ -537,6 +562,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate('Parent directory'),
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: SvgPicture.asset(
|
||||
@@ -601,6 +627,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
switch (_locationStatus.value) {
|
||||
case LocationStatus.bread:
|
||||
return MenuButton(
|
||||
tooltip: translate('Search'),
|
||||
onPressed: () {
|
||||
_locationStatus.value = LocationStatus.fileSearchBar;
|
||||
Future.delayed(
|
||||
@@ -627,6 +654,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
);
|
||||
case LocationStatus.fileSearchBar:
|
||||
return MenuButton(
|
||||
tooltip: translate('Clear'),
|
||||
onPressed: () {
|
||||
onSearchText("", isLocal);
|
||||
_locationStatus.value = LocationStatus.bread;
|
||||
@@ -642,6 +670,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
}
|
||||
}),
|
||||
MenuButton(
|
||||
tooltip: translate('Refresh File'),
|
||||
padding: EdgeInsets.only(
|
||||
left: 3,
|
||||
),
|
||||
@@ -667,6 +696,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||
children: [
|
||||
MenuButton(
|
||||
tooltip: translate('Home'),
|
||||
padding: EdgeInsets.only(
|
||||
right: 3,
|
||||
),
|
||||
@@ -682,11 +712,27 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate('Create Folder'),
|
||||
onPressed: () {
|
||||
final name = TextEditingController();
|
||||
String? errorText;
|
||||
_ffi.dialogManager.show((setState, close, context) {
|
||||
name.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
submit() {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
if (!PathUtil.validName(name.value.text,
|
||||
controller.options.value.isWindows)) {
|
||||
setState(() {
|
||||
errorText = translate("Invalid folder name");
|
||||
});
|
||||
return;
|
||||
}
|
||||
controller.createDir(PathUtil.join(
|
||||
controller.directory.value.path,
|
||||
name.value.text,
|
||||
@@ -718,6 +764,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
labelText: translate(
|
||||
"Please enter the folder name",
|
||||
),
|
||||
errorText: errorText,
|
||||
),
|
||||
controller: name,
|
||||
autofocus: true,
|
||||
@@ -751,6 +798,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
),
|
||||
Obx(() => MenuButton(
|
||||
tooltip: translate('Delete'),
|
||||
onPressed: SelectedItems.valid(selectedItems.items)
|
||||
? () async {
|
||||
await (controller
|
||||
@@ -770,6 +818,66 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isWeb)
|
||||
Obx(() => ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||
isLocal
|
||||
? EdgeInsets.only(left: 10)
|
||||
: EdgeInsets.only(right: 10)),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
selectedItems.items.isEmpty
|
||||
? MyTheme.accent80
|
||||
: MyTheme.accent,
|
||||
),
|
||||
),
|
||||
onPressed: () =>
|
||||
{webselectFiles(is_folder: isUploadFolder.value)},
|
||||
label: InkWell(
|
||||
hoverColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
onTapDown: (e) {
|
||||
final x = e.globalPosition.dx;
|
||||
final y = e.globalPosition.dy;
|
||||
uploadButtonTapPosition =
|
||||
RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
onTap: () async {
|
||||
final value = await showMenu<bool>(
|
||||
context: context,
|
||||
position: uploadButtonTapPosition,
|
||||
items: [
|
||||
PopupMenuItem<bool>(
|
||||
value: false,
|
||||
child: Text(translate('Upload files')),
|
||||
),
|
||||
PopupMenuItem<bool>(
|
||||
value: true,
|
||||
child: Text(translate('Upload folder')),
|
||||
),
|
||||
]);
|
||||
if (value != null) {
|
||||
isUploadFolder.value = value;
|
||||
bind.mainSetLocalOption(
|
||||
key: 'upload-folder-button',
|
||||
value: value ? 'Y' : '');
|
||||
webselectFiles(is_folder: value);
|
||||
}
|
||||
},
|
||||
child: Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
icon: Text(
|
||||
translate(isUploadFolder.isTrue
|
||||
? 'Upload folder'
|
||||
: 'Upload files'),
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
).marginOnly(left: 8),
|
||||
)).marginOnly(left: 16),
|
||||
Obx(() => ElevatedButton.icon(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||
@@ -803,19 +911,22 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
: Colors.white,
|
||||
),
|
||||
)
|
||||
: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
colorFilter: svgColor(selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? MyTheme.grayBg
|
||||
: MyTheme.darkGray
|
||||
: Colors.white),
|
||||
alignment: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
: isWeb
|
||||
? Offstage()
|
||||
: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
colorFilter: svgColor(
|
||||
selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? MyTheme.grayBg
|
||||
: MyTheme.darkGray
|
||||
: Colors.white),
|
||||
alignment: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
label: isLocal
|
||||
? SvgPicture.asset(
|
||||
"assets/arrow.svg",
|
||||
@@ -827,7 +938,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
: Colors.white),
|
||||
)
|
||||
: Text(
|
||||
translate('Receive'),
|
||||
translate(isWeb ? 'Download' : 'Receive'),
|
||||
style: TextStyle(
|
||||
color: selectedItems.items.isEmpty
|
||||
? Theme.of(context).brightness ==
|
||||
@@ -882,6 +993,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
child: MenuButton(
|
||||
tooltip: translate('More'),
|
||||
onPressed: () => mod_menu.showMenu(
|
||||
context: context,
|
||||
position: menuPos,
|
||||
@@ -913,6 +1025,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
BuildContext context, ScrollController scrollController) {
|
||||
final fd = controller.directory.value;
|
||||
final entries = fd.entries;
|
||||
Rx<Entry?> rightClickEntry = Rx(null);
|
||||
|
||||
return ListSearchActionListener(
|
||||
node: _keyboardNode,
|
||||
@@ -971,16 +1084,70 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
final lastModifiedStr = entry.isDrive
|
||||
? " "
|
||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
onTap() {
|
||||
final items = selectedItems;
|
||||
// handle double click
|
||||
if (_checkDoubleClick(entry)) {
|
||||
controller.openDirectory(entry.path);
|
||||
items.clear();
|
||||
return;
|
||||
}
|
||||
_onSelectedChanged(items, filteredEntries, entry, isLocal);
|
||||
}
|
||||
|
||||
onSecondaryTap() {
|
||||
final items = [
|
||||
if (!entry.isDrive &&
|
||||
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
|
||||
mod_menu.PopupMenuItem(
|
||||
child: Text(translate("Rename")),
|
||||
height: CustomPopupMenuTheme.height,
|
||||
onTap: () {
|
||||
controller.renameAction(entry, isLocal);
|
||||
},
|
||||
)
|
||||
];
|
||||
if (items.isNotEmpty) {
|
||||
rightClickEntry.value = entry;
|
||||
final future = mod_menu.showMenu(
|
||||
context: context,
|
||||
position: secondaryPosition,
|
||||
items: items,
|
||||
);
|
||||
future.then((value) {
|
||||
rightClickEntry.value = null;
|
||||
});
|
||||
future.onError((error, stackTrace) {
|
||||
rightClickEntry.value = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSecondaryTapDown(details) {
|
||||
secondaryPosition = RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 1),
|
||||
child: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: selectedItems.items.contains(entry)
|
||||
? Theme.of(context).hoverColor
|
||||
? MyTheme.button
|
||||
: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5.0),
|
||||
),
|
||||
border: rightClickEntry.value == entry
|
||||
? Border.all(
|
||||
color: MyTheme.button,
|
||||
width: 1.0,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
key: ValueKey(entry.name),
|
||||
height: kDesktopFileTransferRowHeight,
|
||||
@@ -1019,22 +1186,19 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
),
|
||||
Expanded(
|
||||
child: Text(entry.name.nonBreaking,
|
||||
style: TextStyle(
|
||||
color: selectedItems.items
|
||||
.contains(entry)
|
||||
? Colors.white
|
||||
: null),
|
||||
overflow:
|
||||
TextOverflow.ellipsis))
|
||||
]),
|
||||
)),
|
||||
),
|
||||
onTap: () {
|
||||
final items = selectedItems;
|
||||
// handle double click
|
||||
if (_checkDoubleClick(entry)) {
|
||||
controller.openDirectory(entry.path);
|
||||
items.clear();
|
||||
return;
|
||||
}
|
||||
_onSelectedChanged(
|
||||
items, filteredEntries, entry, isLocal);
|
||||
},
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2.0,
|
||||
@@ -1051,11 +1215,17 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
color: selectedItems.items
|
||||
.contains(entry)
|
||||
? Colors.white70
|
||||
: MyTheme.darkGray,
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
// Divider from header.
|
||||
SizedBox(
|
||||
@@ -1071,9 +1241,16 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
sizeStr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10, color: MyTheme.darkGray),
|
||||
fontSize: 10,
|
||||
color:
|
||||
selectedItems.items.contains(entry)
|
||||
? Colors.white70
|
||||
: MyTheme.darkGray),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -34,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@@ -47,6 +48,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -54,10 +56,8 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
debugPrint(
|
||||
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewFileTransfer) {
|
||||
@@ -77,6 +77,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
)));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
@@ -91,23 +92,28 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton().paddingOnly(left: 10),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
)),
|
||||
);
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
));
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
);
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
);
|
||||
}
|
||||
@@ -128,9 +134,9 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
tabController.clear();
|
||||
return true;
|
||||
} else {
|
||||
final opt = "enable-confirm-closing-tabs";
|
||||
final bool res;
|
||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
||||
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||
res = true;
|
||||
} else {
|
||||
res = await closeConfirmDialog();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
|
||||
class _InstallPageState extends State<InstallPage> {
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_InstallPageState() {
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
const label = "install";
|
||||
tabController.add(TabInfo(
|
||||
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return DragToResizeArea(
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||
child: Container(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
||||
);
|
||||
|
||||
_InstallPageBodyState() {
|
||||
controller = TextEditingController(text: bind.installInstallPath());
|
||||
final installOptions = jsonDecode(bind.installInstallOptions());
|
||||
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
|
||||
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
controller = TextEditingController(text: bind.installInstallPath());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
if (desktopicon.value) args += ' desktopicon';
|
||||
bind.installInstallMe(options: args, path: controller.text);
|
||||
}
|
||||
|
||||
do_install();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget {
|
||||
required this.isRDP,
|
||||
required this.isSharedPassword,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
@@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget {
|
||||
final bool isRDP;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
|
||||
@override
|
||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||
@@ -62,10 +64,14 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
isRdp: widget.isRDP);
|
||||
Get.put(_ffi, tag: 'pf_${widget.id}');
|
||||
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||
debugPrint("Port forward page init success with id ${widget.id}");
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -141,8 +147,9 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
||||
|
||||
return Theme(
|
||||
data: Theme.of(context)
|
||||
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: Theme.of(context).colorScheme,
|
||||
),
|
||||
child: Obx(() => ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemCount: pfs.length + 2,
|
||||
@@ -289,7 +296,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
).marginOnly(left: _kTextLeftMargin));
|
||||
return Theme(
|
||||
data: Theme.of(context)
|
||||
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
|
||||
.copyWith(colorScheme: Theme.of(context).colorScheme),
|
||||
child: ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemCount: 2,
|
||||
|
||||
@@ -34,6 +34,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@@ -47,6 +48,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
tabController: tabController,
|
||||
isRDP: isRDP,
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -54,8 +56,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
debugPrint(
|
||||
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
@@ -83,6 +83,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
isRDP: isRDP,
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
)));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
@@ -97,27 +98,38 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: () async {
|
||||
tabController.clear();
|
||||
return true;
|
||||
},
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
)),
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: () async {
|
||||
tabController.clear();
|
||||
return true;
|
||||
},
|
||||
tail: AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
),
|
||||
);
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(
|
||||
context,
|
||||
Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: child),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
);
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(
|
||||
() => SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
@@ -15,7 +15,6 @@ 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';
|
||||
import '../../common/shared_state.dart';
|
||||
import '../../utils/image.dart';
|
||||
@@ -45,7 +44,9 @@ class RemotePage extends StatefulWidget {
|
||||
this.switchUuid,
|
||||
this.forceRelay,
|
||||
this.isSharedPassword,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key) {
|
||||
initSharedStates(id);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final SessionID? sessionId;
|
||||
@@ -64,7 +65,7 @@ class RemotePage extends StatefulWidget {
|
||||
|
||||
@override
|
||||
State<RemotePage> createState() {
|
||||
final state = _RemotePageState();
|
||||
final state = _RemotePageState(id);
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
@@ -85,15 +86,21 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||
|
||||
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
||||
// to identify the toolbar instance and its callback function.
|
||||
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
||||
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
||||
|
||||
late FFI _ffi;
|
||||
|
||||
SessionID get sessionId => _ffi.sessionId;
|
||||
|
||||
_RemotePageState(String id) {
|
||||
_initStates(id);
|
||||
}
|
||||
|
||||
void _initStates(String id) {
|
||||
initSharedStates(id);
|
||||
_zoomCursor = PeerBoolOption.find(id, 'zoom-cursor');
|
||||
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||
@@ -102,12 +109,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initStates(widget.id);
|
||||
_ffi = FFI(widget.sessionId);
|
||||
Get.put(_ffi, tag: widget.id);
|
||||
Get.put<FFI>(_ffi, tag: widget.id);
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
_ffi.recordingModel
|
||||
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
||||
});
|
||||
_ffi.start(
|
||||
widget.id,
|
||||
@@ -131,11 +139,14 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
// Session option should be set after models.dart/FFI.start
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'zoom-cursor');
|
||||
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Session option should be set after models.dart/FFI.start
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||
});
|
||||
DesktopMultiWindow.addListener(this);
|
||||
// if (!_isCustomCursorInited) {
|
||||
// customCursorController.registerNeedUpdateCursorCallback(
|
||||
@@ -150,7 +161,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
// }
|
||||
|
||||
_blockableOverlayState.applyFfi(_ffi);
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -165,6 +179,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
// and let OS to handle events instead.
|
||||
_rawKeyFocusNode.unfocus();
|
||||
}
|
||||
stateGlobal.isFocused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -174,6 +189,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (isWindows) {
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
stateGlobal.isFocused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -206,6 +222,22 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEnterFullScreen() {
|
||||
super.onWindowEnterFullScreen();
|
||||
if (isMacOS) {
|
||||
stateGlobal.setFullscreen(true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowLeaveFullScreen() {
|
||||
super.onWindowLeaveFullScreen();
|
||||
if (isMacOS) {
|
||||
stateGlobal.setFullscreen(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
||||
@@ -214,11 +246,14 @@ class _RemotePageState extends State<RemotePage>
|
||||
super.dispose();
|
||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||
// ensure we leave this session, this is a double check
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
if (closeSession) {
|
||||
// ensure we leave this session, this is a double check
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
}
|
||||
DesktopMultiWindow.removeListener(this);
|
||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||
_ffi.recordingModel.onClose();
|
||||
_ffi.imageModel.disposeImage();
|
||||
_ffi.cursorModel.disposeImages();
|
||||
_rawKeyFocusNode.dispose();
|
||||
await _ffi.close(closeSession: closeSession);
|
||||
_timer?.cancel();
|
||||
@@ -248,9 +283,18 @@ class _RemotePageState extends State<RemotePage>
|
||||
id: widget.id,
|
||||
ffi: _ffi,
|
||||
state: widget.toolbarState,
|
||||
onEnterOrLeaveImageSetter: (func) =>
|
||||
_onEnterOrLeaveImage4Toolbar = func,
|
||||
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
|
||||
onEnterOrLeaveImageSetter: (id, func) {
|
||||
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
|
||||
_onEnterOrLeaveImage4Toolbar = func;
|
||||
},
|
||||
onEnterOrLeaveImageCleaner: (id) {
|
||||
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
|
||||
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
|
||||
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
|
||||
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
|
||||
_onEnterOrLeaveImage4Toolbar = null;
|
||||
}
|
||||
},
|
||||
setRemoteState: setState,
|
||||
);
|
||||
|
||||
@@ -258,7 +302,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.black,
|
||||
color: kColorCanvas,
|
||||
child: RawKeyFocusScope(
|
||||
focusNode: _rawKeyFocusNode,
|
||||
onFocusChange: (bool imageFocused) {
|
||||
@@ -287,8 +331,21 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.ffiModel.waitForFirstImage.isTrue
|
||||
? emptyOverlay()
|
||||
: () {
|
||||
_ffi.ffiModel.tryShowAndroidActionsOverlay();
|
||||
return Offstage();
|
||||
if (!_ffi.ffiModel.isPeerAndroid) {
|
||||
return Offstage();
|
||||
} else {
|
||||
return Obx(() => Offstage(
|
||||
offstage: _ffi.dialogManager
|
||||
.mobileActionsOverlayVisible.isFalse,
|
||||
child: Overlay(initialEntries: [
|
||||
makeMobileActionsOverlayEntry(
|
||||
() => _ffi.dialogManager
|
||||
.setMobileActionsOverlayVisible(false),
|
||||
ffi: _ffi,
|
||||
)
|
||||
]),
|
||||
));
|
||||
}
|
||||
}(),
|
||||
// Use Overlay to enable rebuild every time on menu button click.
|
||||
_ffi.ffiModel.pi.isSet.isTrue
|
||||
@@ -435,14 +492,14 @@ class _RemotePageState extends State<RemotePage>
|
||||
}, onExit: (evt) {
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
});
|
||||
final c = Provider.of<CanvasModel>(context, listen: false);
|
||||
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||
return Obx(
|
||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||
? Container(color: Colors.transparent)
|
||||
: Obx(() {
|
||||
widget.toolbarState.initShow(sessionId);
|
||||
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
@@ -460,12 +517,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
];
|
||||
|
||||
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||
paints.add(Obx(() => Offstage(
|
||||
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
|
||||
child: CursorPaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
))));
|
||||
paints
|
||||
.add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
|
||||
? Offstage()
|
||||
: CursorPaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
)));
|
||||
}
|
||||
paints.add(
|
||||
Positioned(
|
||||
@@ -518,11 +576,6 @@ 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);
|
||||
@@ -572,15 +625,15 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
onHover: (evt) {},
|
||||
child: child);
|
||||
});
|
||||
|
||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||
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);
|
||||
final paintWidget =
|
||||
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
c.updateScrollPercent();
|
||||
@@ -598,17 +651,18 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
));
|
||||
} else {
|
||||
if (c.size.width > 0 && c.size.height > 0) {
|
||||
final paintWidget = useTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c,
|
||||
s,
|
||||
Offset(
|
||||
isLinux ? c.x.toInt().toDouble() : c.x,
|
||||
isLinux ? c.y.toInt().toDouble() : c.y,
|
||||
),
|
||||
c.size,
|
||||
isViewOriginal())
|
||||
: _buildScrollAuthNonTextureRender(m, c, s);
|
||||
final paintWidget =
|
||||
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
||||
? _BuildPaintTextureRender(
|
||||
c,
|
||||
s,
|
||||
Offset(
|
||||
isLinux ? c.x.toInt().toDouble() : c.x,
|
||||
isLinux ? c.y.toInt().toDouble() : c.y,
|
||||
),
|
||||
c.size,
|
||||
isViewOriginal())
|
||||
: _buildScrollAutoNonTextureRender(m, c, s);
|
||||
return mouseRegion(child: _buildListener(paintWidget));
|
||||
} else {
|
||||
return Container();
|
||||
@@ -624,7 +678,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScrollAuthNonTextureRender(
|
||||
Widget _buildScrollAutoNonTextureRender(
|
||||
ImageModel m, CanvasModel c, double s) {
|
||||
return CustomPaint(
|
||||
size: Size(c.size.width, c.size.height),
|
||||
@@ -687,12 +741,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
ScrollController horizontal,
|
||||
ScrollController vertical,
|
||||
) {
|
||||
final scrollConfig = CustomMouseWheelScrollConfig(
|
||||
scrollDuration: kDefaultScrollDuration,
|
||||
scrollCurve: Curves.linearToEaseOut,
|
||||
mouseWheelTurnsThrottleTimeMs:
|
||||
kDefaultMouseWheelThrottleDuration.inMilliseconds,
|
||||
scrollAmountMultiplier: kDefaultScrollAmountMultiplier);
|
||||
var widget = child;
|
||||
if (layoutSize.width < size.width) {
|
||||
widget = ScrollConfiguration(
|
||||
@@ -738,36 +786,26 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
);
|
||||
}
|
||||
if (layoutSize.width < size.width) {
|
||||
widget = ImprovedScrolling(
|
||||
scrollController: horizontal,
|
||||
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
|
||||
customMouseWheelScrollConfig: scrollConfig,
|
||||
child: RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: horizontal,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
notificationPredicate: layoutSize.height < size.height
|
||||
? (notification) => notification.depth == 1
|
||||
: defaultScrollNotificationPredicate,
|
||||
child: widget,
|
||||
),
|
||||
widget = RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: horizontal,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
notificationPredicate: layoutSize.height < size.height
|
||||
? (notification) => notification.depth == 1
|
||||
: defaultScrollNotificationPredicate,
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
if (layoutSize.height < size.height) {
|
||||
widget = ImprovedScrolling(
|
||||
scrollController: vertical,
|
||||
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
|
||||
customMouseWheelScrollConfig: scrollConfig,
|
||||
child: RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: vertical,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
child: widget,
|
||||
),
|
||||
widget = RawScrollbar(
|
||||
thickness: kScrollbarThickness,
|
||||
thumbColor: Colors.grey,
|
||||
controller: vertical,
|
||||
thumbVisibility: false,
|
||||
trackVisibility: false,
|
||||
child: widget,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
@@ -45,7 +46,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||
|
||||
late ToolbarState _toolbarState;
|
||||
String? peerId;
|
||||
bool _isScreenRectSet = false;
|
||||
int? _display;
|
||||
@@ -53,7 +53,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||
|
||||
_ConnectionTabPageState(Map<String, dynamic> params) {
|
||||
_toolbarState = ToolbarState();
|
||||
RemoteCountState.init();
|
||||
peerId = params['id'];
|
||||
final sessionId = params['session_id'];
|
||||
@@ -72,7 +71,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final ffi = remotePage.ffi;
|
||||
bind.setCurSessionId(sessionId: ffi.sessionId);
|
||||
}
|
||||
WindowController.fromWindowId(windowId())
|
||||
WindowController.fromWindowId(params['windowId'])
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
UnreadChatCountState.find(id).value = 0;
|
||||
};
|
||||
@@ -90,7 +89,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: params['password'],
|
||||
toolbarState: _toolbarState,
|
||||
toolbarState: ToolbarState(),
|
||||
tabController: tabController,
|
||||
switchUuid: params['switch_uuid'],
|
||||
forceRelay: params['forceRelay'],
|
||||
@@ -99,115 +98,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
));
|
||||
_update_remote_count();
|
||||
}
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
|
||||
dynamic returnValue;
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewRemoteDesktop) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
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) {
|
||||
// Show the hidden window.
|
||||
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
|
||||
stateGlobal.setFullscreen(true);
|
||||
}
|
||||
// Reset the state
|
||||
stateGlobal.closeOnFullscreen = null;
|
||||
}
|
||||
ConnectionTypeState.init(id);
|
||||
_toolbarState.setShow(
|
||||
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
||||
tabController.add(TabInfo(
|
||||
key: id,
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
switchUuid: switchUuid,
|
||||
forceRelay: args['forceRelay'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
),
|
||||
));
|
||||
} else if (call.method == kWindowDisableGrabKeyboard) {
|
||||
// ???
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventActiveSession) {
|
||||
final jumpOk = tabController.jumpToByKey(call.arguments);
|
||||
if (jumpOk) {
|
||||
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)
|
||||
.toList()
|
||||
.join(',');
|
||||
} else if (call.method == kWindowEventGetSessionIdList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
|
||||
.toList()
|
||||
.join(';');
|
||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||
// Ready to show new window and close old tab.
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final close = args['close'];
|
||||
try {
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.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 (close && returnValue != null) {
|
||||
closeSessionOnDispose[id] = false;
|
||||
tabController.closeBy(id);
|
||||
}
|
||||
}
|
||||
_update_remote_count();
|
||||
return returnValue;
|
||||
});
|
||||
if (!_isScreenRectSet) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(
|
||||
@@ -222,111 +120,106 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_toolbarState.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Obx(
|
||||
() => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: MyTheme.color(context).border!,
|
||||
width: stateGlobal.windowBorderWidth.value),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton().paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
final connectionType = ConnectionTypeState.find(key);
|
||||
if (!connectionType.isValid()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
label,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
bool secure =
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
String msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
if (fingerprint.isEmpty) {
|
||||
fingerprint = 'N/A';
|
||||
}
|
||||
if (fingerprint.length > 5 * 8) {
|
||||
var first = fingerprint.substring(0, 39);
|
||||
var second = fingerprint.substring(40);
|
||||
msgFingerprint += '$first\n$second';
|
||||
} else {
|
||||
msgFingerprint += fingerprint;
|
||||
}
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
final connectionType = ConnectionTypeState.find(key);
|
||||
if (!connectionType.isValid()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
label,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
bool secure =
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
String msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
if (fingerprint.isEmpty) {
|
||||
fingerprint = 'N/A';
|
||||
}
|
||||
if (fingerprint.length > 5 * 8) {
|
||||
var first = fingerprint.substring(0, 39);
|
||||
var second = fingerprint.substring(40);
|
||||
msgFingerprint += '$first\n$second';
|
||||
} else {
|
||||
msgFingerprint += fingerprint;
|
||||
}
|
||||
|
||||
final tab = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Tooltip(
|
||||
message: '$msgConn\n$msgFingerprint',
|
||||
child: SvgPicture.asset(
|
||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
||||
width: themeConf.iconSize,
|
||||
height: themeConf.iconSize,
|
||||
).paddingOnly(right: 5),
|
||||
),
|
||||
label,
|
||||
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
||||
.marginOnly(left: 4),
|
||||
],
|
||||
);
|
||||
final tab = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Tooltip(
|
||||
message: '$msgConn\n$msgFingerprint',
|
||||
child: SvgPicture.asset(
|
||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
||||
width: themeConf.iconSize,
|
||||
height: themeConf.iconSize,
|
||||
).paddingOnly(right: 5),
|
||||
),
|
||||
label,
|
||||
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
||||
.marginOnly(left: 4),
|
||||
],
|
||||
);
|
||||
|
||||
return Listener(
|
||||
onPointerDown: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
return;
|
||||
}
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == key)
|
||||
.page as RemotePage;
|
||||
if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
|
||||
e.buttons == 2) {
|
||||
showRightMenu(
|
||||
(CancelFunc cancelFunc) {
|
||||
return _tabMenuBuilder(key, cancelFunc);
|
||||
},
|
||||
target: e.position,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: tab,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
return Listener(
|
||||
onPointerDown: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
return;
|
||||
}
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == key)
|
||||
.page as RemotePage;
|
||||
if (remotePage.ffi.ffiModel.pi.isSet.isTrue && e.buttons == 2) {
|
||||
showRightMenu(
|
||||
(CancelFunc cancelFunc) {
|
||||
return _tabMenuBuilder(key, cancelFunc);
|
||||
},
|
||||
target: e.position,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: tab,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: MyTheme.color(context).border!,
|
||||
width: stateGlobal.windowBorderWidth.value),
|
||||
),
|
||||
child: child,
|
||||
));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(() => SubWindowDragToResizeArea(
|
||||
@@ -335,6 +228,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
// Specially configured for a better resize area and remote control.
|
||||
childPadding: kDragToResizeAreaPadding,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
));
|
||||
}
|
||||
@@ -350,15 +244,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final pi = ffi.ffiModel.pi;
|
||||
final perms = ffi.ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
final toolbarState = remotePage.toolbarState;
|
||||
menu.addAll([
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||
translate(
|
||||
_toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
style: style,
|
||||
)),
|
||||
proc: () {
|
||||
_toolbarState.switchShow();
|
||||
toolbarState.switchShow(sessionId);
|
||||
cancelFunc();
|
||||
},
|
||||
padding: padding,
|
||||
@@ -449,7 +344,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
void onRemoveId(String id) async {
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
// Keep calling until the window status is hidden.
|
||||
//
|
||||
// Workaround for Windows:
|
||||
@@ -483,9 +377,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
tabController.clear();
|
||||
return true;
|
||||
} else {
|
||||
final opt = "enable-confirm-closing-tabs";
|
||||
final bool res;
|
||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
||||
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||
res = true;
|
||||
} else {
|
||||
res = await closeConfirmDialog();
|
||||
@@ -499,4 +393,132 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
_update_remote_count() =>
|
||||
RemoteCountState.find().value = tabController.length;
|
||||
|
||||
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
||||
debugPrint(
|
||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
|
||||
dynamic returnValue;
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewRemoteDesktop) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
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);
|
||||
final prePeerCount = tabController.length;
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (stateGlobal.fullscreen.isTrue) {
|
||||
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
}
|
||||
await setNewConnectWindowFrame(
|
||||
windowId(), id!, prePeerCount, display, screenRect);
|
||||
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||
await windowOnTop(windowId());
|
||||
});
|
||||
});
|
||||
ConnectionTypeState.init(id);
|
||||
tabController.add(TabInfo(
|
||||
key: id,
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
toolbarState: ToolbarState(),
|
||||
tabController: tabController,
|
||||
switchUuid: switchUuid,
|
||||
forceRelay: args['forceRelay'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
),
|
||||
));
|
||||
} else if (call.method == kWindowDisableGrabKeyboard) {
|
||||
// ???
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventActiveSession) {
|
||||
final jumpOk = tabController.jumpToByKey(call.arguments);
|
||||
if (jumpOk) {
|
||||
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)
|
||||
.toList()
|
||||
.join(',');
|
||||
} else if (call.method == kWindowEventGetSessionIdList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
|
||||
.toList()
|
||||
.join(';');
|
||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||
// Ready to show new window and close old tab.
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final close = args['close'];
|
||||
try {
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.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 (close && returnValue != null) {
|
||||
closeSessionOnDispose[id] = false;
|
||||
tabController.closeBy(id);
|
||||
}
|
||||
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||
final remotePage =
|
||||
tabController.state.value.selectedTabInfo.page as RemotePage;
|
||||
final ffi = remotePage.ffi;
|
||||
final displayRect = ffi.ffiModel.displaysRect();
|
||||
if (displayRect != null) {
|
||||
final wc = WindowController.fromWindowId(windowId());
|
||||
Rect? frame;
|
||||
try {
|
||||
frame = await wc.getFrame();
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
"Failed to get frame of window $windowId, it may be hidden");
|
||||
}
|
||||
if (frame != null) {
|
||||
ffi.cursorModel.moveLocal(0, 0);
|
||||
final coords = RemoteWindowCoords(
|
||||
frame,
|
||||
CanvasCoords.fromCanvasModel(ffi.canvasModel),
|
||||
CursorCoords.fromCursorModel(ffi.cursorModel),
|
||||
displayRect);
|
||||
returnValue = jsonEncode(coords.toJson());
|
||||
}
|
||||
}
|
||||
} else if (call.method == kWindowEventSetFullscreen) {
|
||||
stateGlobal.setFullscreen(call.arguments == 'true');
|
||||
}
|
||||
_update_remote_count();
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
@@ -31,14 +32,18 @@ class DesktopServerPage extends StatefulWidget {
|
||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
final tabController = gFFI.serverModel.tabController;
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
_DesktopServerPageState() {
|
||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
||||
windowManager.addListener(this);
|
||||
Get.put(tabController);
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
tabController.onRemoved = (_, id) {
|
||||
onRemoveId(id);
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -76,14 +81,20 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
ChangeNotifierProvider.value(value: gFFI.chatModel),
|
||||
],
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, serverModel, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
builder: (context, serverModel, child) {
|
||||
final body = Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: ConnectionManager(),
|
||||
),
|
||||
),
|
||||
);
|
||||
return isLinux
|
||||
? buildVirtualWindowFrame(context, body)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: MyTheme.color(context).border!)),
|
||||
child: body,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -97,10 +108,11 @@ class ConnectionManager extends StatefulWidget {
|
||||
State<StatefulWidget> createState() => ConnectionManagerState();
|
||||
}
|
||||
|
||||
class ConnectionManagerState extends State<ConnectionManager> {
|
||||
@override
|
||||
void initState() {
|
||||
gFFI.serverModel.updateClientState();
|
||||
class ConnectionManagerState extends State<ConnectionManager>
|
||||
with WidgetsBindingObserver {
|
||||
final RxBool _block = false.obs;
|
||||
|
||||
ConnectionManagerState() {
|
||||
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
||||
final client_id = int.tryParse(client_id_str);
|
||||
if (client_id != null) {
|
||||
@@ -109,7 +121,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
if (client != null) {
|
||||
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
||||
if (client.unreadChatMessageCount.value > 0) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
client.unreadChatMessageCount.value = 0;
|
||||
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
||||
});
|
||||
@@ -120,9 +132,31 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
}
|
||||
};
|
||||
gFFI.chatModel.isConnManager = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (!allowRemoteCMModification()) {
|
||||
shouldBeBlocked(_block, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
gFFI.serverModel.updateClientState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
@@ -157,9 +191,8 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
controller: serverModel.tabController,
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
maxLabelWidth: 100,
|
||||
tail: buildScrollJumper(),
|
||||
selectedTabBackgroundColor:
|
||||
Theme.of(context).hintColor.withOpacity(0),
|
||||
tail: null, //buildScrollJumper(),
|
||||
blockTab: allowRemoteCMModification() ? null : _block,
|
||||
tabBuilder: (key, icon, label, themeConf) {
|
||||
final client = serverModel.clients
|
||||
.firstWhereOrNull((client) => client.id.toString() == key);
|
||||
@@ -194,27 +227,28 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
borderWidth;
|
||||
final realChatPageWidth =
|
||||
constrains.maxWidth - realClosedWidth;
|
||||
return Row(children: [
|
||||
final row = Row(children: [
|
||||
if (constrains.maxWidth >
|
||||
kConnectionManagerWindowSizeClosedChat.width)
|
||||
Consumer<ChatModel>(
|
||||
builder: (_, model, child) => SizedBox(
|
||||
width: realChatPageWidth,
|
||||
child: buildRemoteBlock(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.dividerColor))),
|
||||
child: buildSidePage()),
|
||||
),
|
||||
child: allowRemoteCMModification()
|
||||
? buildSidePage()
|
||||
: buildRemoteBlock(
|
||||
child: buildSidePage(),
|
||||
block: _block,
|
||||
mask: true),
|
||||
)),
|
||||
SizedBox(
|
||||
width: realClosedWidth,
|
||||
child:
|
||||
SizedBox(width: realClosedWidth, child: pageView)),
|
||||
]);
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: row,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -282,9 +316,9 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
windowManager.close();
|
||||
return true;
|
||||
} else {
|
||||
final opt = "enable-confirm-closing-tabs";
|
||||
final bool res;
|
||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
||||
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||
res = true;
|
||||
} else {
|
||||
res = await closeConfirmDialog();
|
||||
@@ -374,7 +408,10 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
_time.value = _time.value + 1;
|
||||
}
|
||||
});
|
||||
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -701,17 +738,88 @@ class _CmControlPanel extends StatelessWidget {
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !client.inVoiceCall,
|
||||
child: buildButton(
|
||||
context,
|
||||
color: Colors.red,
|
||||
onClick: () => closeVoiceCall(),
|
||||
icon: Icon(
|
||||
Icons.call_end_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Stop voice call",
|
||||
textColor: Colors.white,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: buildButton(context,
|
||||
color: MyTheme.accent,
|
||||
onClick: null, onTapDown: (details) async {
|
||||
final devicesInfo =
|
||||
await AudioInput.getDevicesInfo(true, true);
|
||||
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
msgBox(
|
||||
gFFI.sessionId,
|
||||
'custom-nocancel-info',
|
||||
'Prompt',
|
||||
'no_audio_input_device_tip',
|
||||
'',
|
||||
gFFI.dialogManager,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String currentDevice = devicesInfo['current'] as String;
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
final position = RelativeRect.fromLTRB(x, y, x, y);
|
||||
showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: devices
|
||||
.map((d) => PopupMenuItem<String>(
|
||||
value: d,
|
||||
height: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () => AudioInput.setDevice(d, true, true),
|
||||
child: IgnorePointer(
|
||||
child: RadioMenuButton(
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null)
|
||||
AudioInput.setDevice(v, true, true);
|
||||
},
|
||||
child: Container(
|
||||
child: Text(
|
||||
d,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth:
|
||||
kConnectionManagerWindowSizeClosedChat
|
||||
.width -
|
||||
80),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.call_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Audio input",
|
||||
textColor: Colors.white),
|
||||
),
|
||||
Expanded(
|
||||
child: buildButton(
|
||||
context,
|
||||
color: Colors.red,
|
||||
onClick: () => closeVoiceCall(),
|
||||
icon: Icon(
|
||||
Icons.call_end_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Stop voice call",
|
||||
textColor: Colors.white,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
@@ -872,12 +980,14 @@ class _CmControlPanel extends StatelessWidget {
|
||||
|
||||
Widget buildButton(BuildContext context,
|
||||
{required Color? color,
|
||||
required Function() onClick,
|
||||
Icon? icon,
|
||||
GestureTapCallback? onClick,
|
||||
Widget? icon,
|
||||
BoxBorder? border,
|
||||
required String text,
|
||||
required Color? textColor,
|
||||
String? tooltip}) {
|
||||
String? tooltip,
|
||||
GestureTapDownCallback? onTapDown}) {
|
||||
assert(!(onClick == null && onTapDown == null));
|
||||
Widget textWidget;
|
||||
if (icon != null) {
|
||||
textWidget = Text(
|
||||
@@ -901,7 +1011,16 @@ class _CmControlPanel extends StatelessWidget {
|
||||
color: color, borderRadius: borderRadius, border: border),
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: () => checkClickTime(client.id, onClick),
|
||||
onTap: () {
|
||||
if (onClick == null) return;
|
||||
checkClickTime(client.id, onClick);
|
||||
},
|
||||
onTapDown: (details) {
|
||||
if (onTapDown == null) return;
|
||||
checkClickTime(client.id, () {
|
||||
onTapDown.call(details);
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -956,6 +1075,10 @@ class _CmControlPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
void checkClickTime(int id, Function() callback) async {
|
||||
if (allowRemoteCMModification()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
||||
await bind.cmCheckClickTime(connId: id);
|
||||
Timer(const Duration(milliseconds: 120), () async {
|
||||
@@ -964,6 +1087,11 @@ void checkClickTime(int id, Function() callback) async {
|
||||
});
|
||||
}
|
||||
|
||||
bool allowRemoteCMModification() {
|
||||
return option2bool(kOptionAllowRemoteCmModification,
|
||||
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
|
||||
}
|
||||
|
||||
class _FileTransferLogPage extends StatefulWidget {
|
||||
_FileTransferLogPage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -1029,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
Text(translate('Create Folder'))
|
||||
],
|
||||
);
|
||||
case CmFileAction.rename:
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.drive_file_move_outlined,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
Text(translate('Rename'))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class DesktopFileTransferScreen extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: FileManagerTabPage(
|
||||
params: params,
|
||||
),
|
||||
|
||||
@@ -17,6 +17,7 @@ class DesktopPortForwardScreen extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: PortForwardTabPage(
|
||||
params: params,
|
||||
),
|
||||
|
||||
@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
|
||||
localPlatform = kPeerPlatformWindows;
|
||||
} else if (isLinux) {
|
||||
localPlatform = kPeerPlatformLinux;
|
||||
} else if (isWebOnWindows || isWebOnLinux) {
|
||||
localPlatform = kPeerPlatformWebDesktop;
|
||||
}
|
||||
// to-do: web desktop support ?
|
||||
return localPlatform;
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
|
||||
/// The text style of the popup menu item.
|
||||
///
|
||||
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
|
||||
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1]
|
||||
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium]
|
||||
/// of [ThemeData.textTheme] is used.
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@@ -341,8 +341,9 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
||||
@protected
|
||||
void handleTap() {
|
||||
widget.onTap?.call();
|
||||
|
||||
Navigator.pop<T>(context, widget.value);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop<T>(context, widget.value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -351,7 +352,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||
TextStyle style = widget.textStyle ??
|
||||
popupMenuTheme.textStyle ??
|
||||
theme.textTheme.subtitle1!;
|
||||
theme.textTheme.titleMedium!;
|
||||
|
||||
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: widget.tooltip,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
|
||||
@@ -38,18 +38,16 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
|
||||
@override
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(enabled?.value);
|
||||
}
|
||||
|
||||
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
extends State<W> {
|
||||
RxBool enabled = true.obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.enabled != null) {
|
||||
enabled.value = widget.enabled!.value;
|
||||
MyPopupMenuItemState(bool? e) {
|
||||
if (e != null) {
|
||||
enabled.value = e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +63,7 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||
TextStyle style = widget.textStyle ??
|
||||
popupMenuTheme.textStyle ??
|
||||
theme.textTheme.subtitle1!;
|
||||
theme.textTheme.titleMedium!;
|
||||
return Obx(() => mod_menu.PopupMenuButton<T>(
|
||||
enabled: enabled.value,
|
||||
position: widget.position,
|
||||
@@ -445,9 +443,18 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
dismissCallback: dismissCallback,
|
||||
);
|
||||
|
||||
bool get isEnabled => enabled?.value ?? true;
|
||||
|
||||
RxBool get curOption;
|
||||
Future<void> setOption(bool? option);
|
||||
|
||||
tryPop(BuildContext context) {
|
||||
if (dismissOnClicked && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
super.dismissCallback?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<mod_menu.PopupMenuEntry<T>> build(
|
||||
BuildContext context, MenuConfig conf) {
|
||||
@@ -481,44 +488,33 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
if (switchType == SwitchType.sswitch) {
|
||||
return Switch(
|
||||
value: curOption.value,
|
||||
onChanged: (v) {
|
||||
if (super.dismissOnClicked &&
|
||||
Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
if (super.dismissCallback != null) {
|
||||
super.dismissCallback!();
|
||||
}
|
||||
}
|
||||
setOption(v);
|
||||
},
|
||||
onChanged: isEnabled
|
||||
? (v) {
|
||||
tryPop(context);
|
||||
setOption(v);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
return Checkbox(
|
||||
value: curOption.value,
|
||||
onChanged: (v) {
|
||||
if (super.dismissOnClicked &&
|
||||
Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
if (super.dismissCallback != null) {
|
||||
super.dismissCallback!();
|
||||
}
|
||||
}
|
||||
setOption(v);
|
||||
},
|
||||
onChanged: isEnabled
|
||||
? (v) {
|
||||
tryPop(context);
|
||||
setOption(v);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
})),
|
||||
))
|
||||
])),
|
||||
onPressed: () {
|
||||
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
if (super.dismissCallback != null) {
|
||||
super.dismissCallback!();
|
||||
}
|
||||
}
|
||||
setOption(!curOption.value);
|
||||
},
|
||||
onPressed: isEnabled
|
||||
? () {
|
||||
tryPop(context);
|
||||
setOption(!curOption.value);
|
||||
}
|
||||
: null,
|
||||
)),
|
||||
)
|
||||
];
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||
@@ -26,46 +26,42 @@ import './popup_menu.dart';
|
||||
import './kb_layout_type_chooser.dart';
|
||||
|
||||
class ToolbarState {
|
||||
final kStoreKey = 'remoteMenubarState';
|
||||
late RxBool show;
|
||||
late RxBool _pin;
|
||||
|
||||
bool isShowInited = false;
|
||||
RxBool show = false.obs;
|
||||
|
||||
ToolbarState() {
|
||||
final s = bind.getLocalFlutterOption(k: kStoreKey);
|
||||
_pin = RxBool(false);
|
||||
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
||||
if (s.isEmpty) {
|
||||
_initSet(false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final m = jsonDecode(s);
|
||||
if (m == null) {
|
||||
_initSet(false, false);
|
||||
} else {
|
||||
_initSet(m['pin'] ?? false, m['pin'] ?? false);
|
||||
if (m != null) {
|
||||
_pin = RxBool(m['pin'] ?? false);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to decode toolbar state ${e.toString()}');
|
||||
_initSet(false, false);
|
||||
}
|
||||
}
|
||||
|
||||
_initSet(bool s, bool p) {
|
||||
// Show remubar when connection is established.
|
||||
show =
|
||||
RxBool(bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
||||
_pin = RxBool(p);
|
||||
}
|
||||
|
||||
bool get pin => _pin.value;
|
||||
|
||||
switchShow() async {
|
||||
switchShow(SessionID sessionId) async {
|
||||
bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: kOptionCollapseToolbar);
|
||||
show.value = !show.value;
|
||||
}
|
||||
|
||||
setShow(bool v) async {
|
||||
if (show.value != v) {
|
||||
show.value = v;
|
||||
initShow(SessionID sessionId) async {
|
||||
if (!isShowInited) {
|
||||
show.value = !(await bind.sessionGetToggleOption(
|
||||
sessionId: sessionId, arg: kOptionCollapseToolbar) ??
|
||||
false);
|
||||
isShowInited = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,11 +81,7 @@ class ToolbarState {
|
||||
|
||||
_savePin() async {
|
||||
bind.setLocalFlutterOption(
|
||||
k: kStoreKey, v: jsonEncode({'pin': _pin.value}));
|
||||
}
|
||||
|
||||
save() async {
|
||||
await _savePin();
|
||||
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +305,7 @@ class RemoteMenuEntry {
|
||||
}) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
'${translate("Insert")} Ctrl + Alt + Del',
|
||||
translate("Insert Ctrl + Alt + Del"),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
@@ -333,8 +325,8 @@ class RemoteToolbar extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
final ToolbarState state;
|
||||
final Function(Function(bool)) onEnterOrLeaveImageSetter;
|
||||
final VoidCallback onEnterOrLeaveImageCleaner;
|
||||
final Function(int, Function(bool)) onEnterOrLeaveImageSetter;
|
||||
final Function(int) onEnterOrLeaveImageCleaner;
|
||||
final Function(VoidCallback) setRemoteState;
|
||||
|
||||
RemoteToolbar({
|
||||
@@ -380,7 +372,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
initState() {
|
||||
super.initState();
|
||||
|
||||
Future.delayed(Duration.zero, () async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
_fractionX.value = double.tryParse(await bind.sessionGetOption(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
arg: 'remote-menubar-drag-x') ??
|
||||
@@ -394,7 +386,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
initialValue: 0,
|
||||
);
|
||||
|
||||
widget.onEnterOrLeaveImageSetter((enter) {
|
||||
widget.onEnterOrLeaveImageSetter(identityHashCode(this), (enter) {
|
||||
if (enter) {
|
||||
triggerAutoHide();
|
||||
_isCursorOverImage = true;
|
||||
@@ -414,12 +406,11 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
dispose() {
|
||||
super.dispose();
|
||||
|
||||
widget.onEnterOrLeaveImageCleaner();
|
||||
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// No need to use future builder here.
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Obx(() => show.value
|
||||
@@ -445,10 +436,11 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
shadowColor: MyTheme.color(context).shadow,
|
||||
borderRadius: borderRadius,
|
||||
child: _DraggableShowHide(
|
||||
id: widget.id,
|
||||
sessionId: widget.ffi.sessionId,
|
||||
dragging: _dragging,
|
||||
fractionX: _fractionX,
|
||||
show: show,
|
||||
toolbarState: widget.state,
|
||||
setFullscreen: _setFullscreen,
|
||||
setMinimize: _minimize,
|
||||
borderRadius: borderRadius,
|
||||
@@ -461,8 +453,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
|
||||
Widget _buildToolbar(BuildContext context) {
|
||||
final List<Widget> toolbarItems = [];
|
||||
toolbarItems.add(_PinMenu(state: widget.state));
|
||||
if (!isWebDesktop) {
|
||||
toolbarItems.add(_PinMenu(state: widget.state));
|
||||
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
||||
}
|
||||
|
||||
@@ -487,8 +479,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
setFullscreen: _setFullscreen,
|
||||
));
|
||||
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
|
||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||
if (!isWeb) {
|
||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
||||
}
|
||||
if (!isWeb) toolbarItems.add(_RecordMenu());
|
||||
@@ -588,8 +580,8 @@ class _MobileActionMenu extends StatelessWidget {
|
||||
return Obx(() => _IconMenuButton(
|
||||
assetName: 'assets/actions_mobile.svg',
|
||||
tooltip: 'Mobile Actions',
|
||||
onPressed: () =>
|
||||
ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
|
||||
onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
|
||||
!ffi.dialogManager.mobileActionsOverlayVisible.value),
|
||||
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
||||
? _ToolbarTheme.blueColor
|
||||
: _ToolbarTheme.inactiveColor,
|
||||
@@ -615,14 +607,14 @@ class _MonitorMenu extends StatelessWidget {
|
||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
||||
|
||||
bool get supportIndividualWindows =>
|
||||
useTextureRender && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => showMonitorsToolbar
|
||||
? buildMultiMonitorMenu()
|
||||
: Obx(() => buildMonitorMenu());
|
||||
? buildMultiMonitorMenu(context)
|
||||
: Obx(() => buildMonitorMenu(context));
|
||||
|
||||
Widget buildMonitorMenu() {
|
||||
Widget buildMonitorMenu(BuildContext context) {
|
||||
final width = SimpleWrapper<double>(0);
|
||||
final monitorsIcon =
|
||||
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
||||
@@ -636,18 +628,18 @@ class _MonitorMenu extends StatelessWidget {
|
||||
menuStyle: MenuStyle(
|
||||
padding:
|
||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget()]);
|
||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
|
||||
}
|
||||
|
||||
Widget buildMultiMonitorMenu() {
|
||||
return Row(children: buildMonitorList(true));
|
||||
Widget buildMultiMonitorMenu(BuildContext context) {
|
||||
return Row(children: buildMonitorList(context, true));
|
||||
}
|
||||
|
||||
Widget buildMonitorSubmenuWidget() {
|
||||
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(children: buildMonitorList(false)),
|
||||
Row(children: buildMonitorList(context, false)),
|
||||
supportIndividualWindows ? Divider() : Offstage(),
|
||||
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
||||
],
|
||||
@@ -663,7 +655,7 @@ class _MonitorMenu extends StatelessWidget {
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionSetDisplaysAsIndividualWindows(
|
||||
sessionId: ffi.sessionId, value: value ? 'Y' : '');
|
||||
sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
|
||||
},
|
||||
ffi: ffi,
|
||||
child: Text(translate('Show displays as individual windows')));
|
||||
@@ -680,7 +672,7 @@ class _MonitorMenu extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
|
||||
List<Widget> buildMonitorList(bool isMulti) {
|
||||
List<Widget> buildMonitorList(BuildContext context, bool isMulti) {
|
||||
final List<Widget> monitorList = [];
|
||||
final pi = ffi.ffiModel.pi;
|
||||
|
||||
@@ -818,7 +810,11 @@ class _MonitorMenu extends StatelessWidget {
|
||||
}
|
||||
RxInt display = CurrentDisplayState.find(id);
|
||||
if (display.value != i) {
|
||||
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
|
||||
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
||||
bind.sessionGetDisplaysAsIndividualWindows(
|
||||
sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
if (isChooseDisplayToOpenInNewWindow) {
|
||||
openMonitorInNewTabOrWindow(i, ffi.id, pi);
|
||||
} else {
|
||||
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
|
||||
@@ -1037,15 +1033,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
FFI get ffi => widget.ffi;
|
||||
String get id => widget.id;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_screenAdjustor.updateScreen();
|
||||
|
||||
menuChildrenGetter() {
|
||||
final menuChildren = <Widget>[
|
||||
_screenAdjustor.adjustWindow(context),
|
||||
@@ -1058,11 +1048,13 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
ffi: widget.ffi,
|
||||
screenAdjustor: _screenAdjustor,
|
||||
),
|
||||
// We may add this feature if it is needed and we have an EV certificate.
|
||||
// _VirtualDisplayMenu(
|
||||
// id: widget.id,
|
||||
// ffi: widget.ffi,
|
||||
// ),
|
||||
if (showVirtualDisplayMenu(ffi))
|
||||
_SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
|
||||
child: Text(translate("Virtual display")),
|
||||
),
|
||||
cursorToggles(),
|
||||
Divider(),
|
||||
toggles(),
|
||||
];
|
||||
@@ -1207,6 +1199,25 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
});
|
||||
}
|
||||
|
||||
cursorToggles() {
|
||||
return futureBuilder(
|
||||
future: toolbarCursor(context, id, ffi),
|
||||
hasData: (data) {
|
||||
final v = data as List<TToggleMenu>;
|
||||
if (v.isEmpty) return Offstage();
|
||||
return Column(children: [
|
||||
Divider(),
|
||||
...v
|
||||
.map((e) => CkbMenuButton(
|
||||
value: e.value,
|
||||
onChanged: e.onChanged,
|
||||
child: e.child,
|
||||
ffi: ffi))
|
||||
.toList(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
toggles() {
|
||||
return futureBuilder(
|
||||
future: toolbarDisplayToggle(context, id, ffi),
|
||||
@@ -1263,7 +1274,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getLocalResolutionWayland();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_getLocalResolutionWayland();
|
||||
});
|
||||
}
|
||||
|
||||
Rect? scaledRect() {
|
||||
@@ -1540,77 +1553,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _VirtualDisplayMenu extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
|
||||
_VirtualDisplayMenu({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState();
|
||||
}
|
||||
|
||||
class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||
return Offstage();
|
||||
}
|
||||
if (!widget.ffi.ffiModel.pi.isInstalled) {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays;
|
||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
||||
children.add(Obx(() => CkbMenuButton(
|
||||
value: virtualDisplays.contains(i + 1),
|
||||
onChanged: privacyModeState.isNotEmpty
|
||||
? null
|
||||
: (bool? value) async {
|
||||
if (value != null) {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
index: i + 1,
|
||||
on: value);
|
||||
}
|
||||
},
|
||||
child: Text('${translate('Virtual display')} ${i + 1}'),
|
||||
ffi: widget.ffi,
|
||||
)));
|
||||
}
|
||||
children.add(Divider());
|
||||
children.add(Obx(() => MenuButton(
|
||||
onPressed: privacyModeState.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false);
|
||||
},
|
||||
ffi: widget.ffi,
|
||||
child: Text(translate('Plug out all')),
|
||||
)));
|
||||
return _SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
menuChildren: children,
|
||||
child: Text(translate("Virtual display")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyboardMenu extends StatelessWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
@@ -1644,6 +1586,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
viewMode(),
|
||||
Divider(),
|
||||
...toolbarToggles(),
|
||||
...mobileActions(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1670,7 +1613,9 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
// If use flutter to grab keys, we can only use one mode.
|
||||
// Map mode and Legacy mode, at least one of them is supported.
|
||||
String? modeOnly;
|
||||
if (isInputSourceFlutter) {
|
||||
// Keep both map and legacy mode on web at the moment.
|
||||
// TODO: Remove legacy mode after web supports translate mode on web.
|
||||
if (isInputSourceFlutter && isDesktop) {
|
||||
if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
||||
modeOnly = kKeyMapMode;
|
||||
@@ -1773,13 +1718,48 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
? (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId, value: kOptionViewOnly);
|
||||
ffiModel.setViewOnly(id, value);
|
||||
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||
final viewOnly = await bind.sessionGetToggleOption(
|
||||
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, viewOnly ?? value);
|
||||
}
|
||||
: null,
|
||||
ffi: ffi,
|
||||
child: Text(translate('View Mode')));
|
||||
}
|
||||
|
||||
mobileActions() {
|
||||
if (pi.platform != kPeerPlatformAndroid) return [];
|
||||
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
|
||||
if (!enabled) return [];
|
||||
return [
|
||||
Divider(),
|
||||
MenuButton(
|
||||
child: Text(translate('Back')),
|
||||
onPressed: () => ffi.inputModel.onMobileBack(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Home')),
|
||||
onPressed: () => ffi.inputModel.onMobileHome(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Apps')),
|
||||
onPressed: () => ffi.inputModel.onMobileApps(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Volume up')),
|
||||
onPressed: () => ffi.inputModel.onMobileVolumeUp(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Volume down')),
|
||||
onPressed: () => ffi.inputModel.onMobileVolumeDown(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Power')),
|
||||
onPressed: () => ffi.inputModel.onMobilePower(),
|
||||
ffi: ffi),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatMenu extends StatefulWidget {
|
||||
@@ -1801,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Chat',
|
||||
key: chatButtonKey,
|
||||
svg: 'assets/chat.svg',
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
if (isWeb) {
|
||||
return buildTextChatButton();
|
||||
} else {
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Chat',
|
||||
key: chatButtonKey,
|
||||
svg: 'assets/chat.svg',
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
}
|
||||
}
|
||||
|
||||
buildTextChatButton() {
|
||||
return _IconMenuButton(
|
||||
assetName: 'assets/message_24dp_5F6368.svg',
|
||||
tooltip: 'Text chat',
|
||||
key: chatButtonKey,
|
||||
onPressed: _textChatOnPressed,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
);
|
||||
}
|
||||
|
||||
textChat() {
|
||||
return MenuButton(
|
||||
child: Text(translate('Text chat')),
|
||||
ffi: widget.ffi,
|
||||
onPressed: () {
|
||||
RenderBox? renderBox =
|
||||
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
onPressed: _textChatOnPressed);
|
||||
}
|
||||
|
||||
Offset? initPos;
|
||||
if (renderBox != null) {
|
||||
final pos = renderBox.localToGlobal(Offset.zero);
|
||||
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
||||
}
|
||||
|
||||
widget.ffi.chatModel.changeCurrentKey(
|
||||
MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
||||
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||
});
|
||||
_textChatOnPressed() {
|
||||
RenderBox? renderBox =
|
||||
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
Offset? initPos;
|
||||
if (renderBox != null) {
|
||||
final pos = renderBox.localToGlobal(Offset.zero);
|
||||
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
||||
}
|
||||
widget.ffi.chatModel
|
||||
.changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
||||
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||
}
|
||||
|
||||
voiceCall() {
|
||||
@@ -1852,32 +1847,73 @@ class _VoiceCallMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
menuChildrenGetter() {
|
||||
final audioInput = AudioInput(
|
||||
builder: (devices, currentDevice, setDevice) {
|
||||
return Column(
|
||||
children: devices
|
||||
.map((d) => RdoMenuButton<String>(
|
||||
child: Container(
|
||||
child: Text(
|
||||
d,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: 250),
|
||||
),
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) setDevice(v);
|
||||
},
|
||||
ffi: ffi,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
isCm: false,
|
||||
isVoiceCall: true,
|
||||
);
|
||||
return [
|
||||
audioInput,
|
||||
Divider(),
|
||||
MenuButton(
|
||||
child: Text(translate('End call')),
|
||||
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
ffi: ffi,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return Obx(
|
||||
() {
|
||||
final String tooltip;
|
||||
final String icon;
|
||||
switch (ffi.chatModel.voiceCallStatus.value) {
|
||||
case VoiceCallStatus.waitingForResponse:
|
||||
tooltip = "Waiting";
|
||||
icon = "assets/call_wait.svg";
|
||||
break;
|
||||
return buildCallWaiting(context);
|
||||
case VoiceCallStatus.connected:
|
||||
tooltip = "Disconnect";
|
||||
icon = "assets/call_end.svg";
|
||||
break;
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Voice call',
|
||||
svg: 'assets/voice_call.svg',
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: menuChildrenGetter,
|
||||
ffi: ffi,
|
||||
);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
return _IconMenuButton(
|
||||
assetName: icon,
|
||||
tooltip: tooltip,
|
||||
onPressed: () =>
|
||||
bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
color: _ToolbarTheme.redColor,
|
||||
hoverColor: _ToolbarTheme.hoverRedColor);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCallWaiting(BuildContext context) {
|
||||
return _IconMenuButton(
|
||||
assetName: "assets/call_wait.svg",
|
||||
tooltip: "Waiting",
|
||||
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
color: _ToolbarTheme.redColor,
|
||||
hoverColor: _ToolbarTheme.hoverRedColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordMenu extends StatelessWidget {
|
||||
@@ -1888,8 +1924,7 @@ class _RecordMenu extends StatelessWidget {
|
||||
var ffi = Provider.of<FfiModel>(context);
|
||||
var recordingModel = Provider.of<RecordingModel>(context);
|
||||
final visible =
|
||||
(recordingModel.start || ffi.permissions['recording'] != false) &&
|
||||
ffi.pi.currentDisplay != kAllDisplayValue;
|
||||
(recordingModel.start || ffi.permissions['recording'] != false);
|
||||
if (!visible) return Offstage();
|
||||
return _IconMenuButton(
|
||||
assetName: 'assets/rec.svg',
|
||||
@@ -2014,7 +2049,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
final Color hoverColor;
|
||||
final List<Widget> Function() menuChildrenGetter;
|
||||
final MenuStyle? menuStyle;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
final double? width;
|
||||
|
||||
_IconSubmenuButton({
|
||||
@@ -2025,7 +2060,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
required this.color,
|
||||
required this.hoverColor,
|
||||
required this.menuChildrenGetter,
|
||||
required this.ffi,
|
||||
this.ffi,
|
||||
this.menuStyle,
|
||||
this.width,
|
||||
}) : super(key: key);
|
||||
@@ -2107,13 +2142,13 @@ class MenuButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final Widget? trailingIcon;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
MenuButton(
|
||||
{Key? key,
|
||||
this.onPressed,
|
||||
this.trailingIcon,
|
||||
required this.child,
|
||||
required this.ffi})
|
||||
this.ffi})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -2122,7 +2157,9 @@ class MenuButton extends StatelessWidget {
|
||||
key: key,
|
||||
onPressed: onPressed != null
|
||||
? () {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onPressed?.call();
|
||||
}
|
||||
: null,
|
||||
@@ -2135,13 +2172,13 @@ class CkbMenuButton extends StatelessWidget {
|
||||
final bool? value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
const CkbMenuButton(
|
||||
{Key? key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
required this.child,
|
||||
required this.ffi})
|
||||
this.ffi})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -2152,7 +2189,9 @@ class CkbMenuButton extends StatelessWidget {
|
||||
child: child,
|
||||
onChanged: onChanged != null
|
||||
? (bool? value) {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onChanged?.call(value);
|
||||
}
|
||||
: null,
|
||||
@@ -2165,13 +2204,13 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
final T? groupValue;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
const RdoMenuButton({
|
||||
Key? key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.child,
|
||||
required this.ffi,
|
||||
this.ffi,
|
||||
this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -2183,7 +2222,9 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
child: child,
|
||||
onChanged: onChanged != null
|
||||
? (T? value) {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onChanged?.call(value);
|
||||
}
|
||||
: null,
|
||||
@@ -2192,10 +2233,11 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _DraggableShowHide extends StatefulWidget {
|
||||
final String id;
|
||||
final SessionID sessionId;
|
||||
final RxDouble fractionX;
|
||||
final RxBool dragging;
|
||||
final RxBool show;
|
||||
final ToolbarState toolbarState;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
final Function(bool) setFullscreen;
|
||||
@@ -2203,10 +2245,11 @@ class _DraggableShowHide extends StatefulWidget {
|
||||
|
||||
const _DraggableShowHide({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.sessionId,
|
||||
required this.fractionX,
|
||||
required this.dragging,
|
||||
required this.show,
|
||||
required this.toolbarState,
|
||||
required this.setFullscreen,
|
||||
required this.setMinimize,
|
||||
required this.borderRadius,
|
||||
@@ -2222,23 +2265,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
double left = 0.0;
|
||||
double right = 1.0;
|
||||
|
||||
RxBool get show => widget.toolbarState.show;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
||||
final confLeft = double.tryParse(
|
||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-left'));
|
||||
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft));
|
||||
if (confLeft == null) {
|
||||
bind.mainSetLocalOption(
|
||||
key: 'remote-menubar-drag-left', value: left.toString());
|
||||
key: kOptionRemoteMenubarDragLeft, value: left.toString());
|
||||
} else {
|
||||
left = confLeft;
|
||||
}
|
||||
final confRight = double.tryParse(
|
||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-right'));
|
||||
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight));
|
||||
if (confRight == null) {
|
||||
bind.mainSetLocalOption(
|
||||
key: 'remote-menubar-drag-right', value: right.toString());
|
||||
key: kOptionRemoteMenubarDragRight, value: right.toString());
|
||||
} else {
|
||||
right = confRight;
|
||||
}
|
||||
@@ -2290,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
);
|
||||
final isFullscreen = stateGlobal.fullscreen;
|
||||
const double iconSize = 20;
|
||||
|
||||
buttonWrapper(VoidCallback? onPressed, Widget child,
|
||||
{Color hoverColor = _ToolbarTheme.blueColor}) {
|
||||
final bgColor = buttonStyle.backgroundColor?.resolve({});
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
style: buttonStyle.copyWith(
|
||||
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return (bgColor ?? hoverColor).withOpacity(0.15);
|
||||
}
|
||||
return bgColor;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final child = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDraggable(context),
|
||||
Obx(() => TextButton(
|
||||
onPressed: () {
|
||||
Obx(() => buttonWrapper(
|
||||
() {
|
||||
widget.setFullscreen(!isFullscreen.value);
|
||||
},
|
||||
child: Tooltip(
|
||||
Tooltip(
|
||||
message: translate(
|
||||
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
|
||||
child: Icon(
|
||||
@@ -2309,32 +2372,52 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
),
|
||||
),
|
||||
)),
|
||||
Obx(() => Offstage(
|
||||
offstage: isFullscreen.isFalse,
|
||||
child: TextButton(
|
||||
onPressed: () => widget.setMinimize(),
|
||||
child: Tooltip(
|
||||
message: translate('Minimize'),
|
||||
child: Icon(
|
||||
Icons.remove,
|
||||
size: iconSize,
|
||||
if (!isMacOS && !isWebDesktop)
|
||||
Obx(() => Offstage(
|
||||
offstage: isFullscreen.isFalse,
|
||||
child: buttonWrapper(
|
||||
widget.setMinimize,
|
||||
Tooltip(
|
||||
message: translate('Minimize'),
|
||||
child: Icon(
|
||||
Icons.remove,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => setState(() {
|
||||
widget.show.value = !widget.show.value;
|
||||
)),
|
||||
buttonWrapper(
|
||||
() => setState(() {
|
||||
widget.toolbarState.switchShow(widget.sessionId);
|
||||
}),
|
||||
child: Obx((() => Tooltip(
|
||||
message: translate(
|
||||
widget.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
Obx((() => Tooltip(
|
||||
message:
|
||||
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
child: Icon(
|
||||
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
|
||||
show.isTrue ? Icons.expand_less : Icons.expand_more,
|
||||
size: iconSize,
|
||||
),
|
||||
))),
|
||||
),
|
||||
if (isWebDesktop)
|
||||
Obx(() {
|
||||
if (show.isTrue) {
|
||||
return Offstage();
|
||||
} else {
|
||||
return buttonWrapper(
|
||||
() => closeConnection(id: widget.id),
|
||||
Tooltip(
|
||||
message: translate('Close'),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: iconSize,
|
||||
color: _ToolbarTheme.redColor,
|
||||
),
|
||||
),
|
||||
hoverColor: _ToolbarTheme.redColor,
|
||||
).paddingOnly(left: iconSize / 2);
|
||||
}
|
||||
})
|
||||
],
|
||||
);
|
||||
return TextButtonTheme(
|
||||
@@ -2370,10 +2453,11 @@ class InputModeMenu {
|
||||
|
||||
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
|
||||
|
||||
Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
|
||||
Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
|
||||
return Listener(
|
||||
onPointerHover: (PointerHoverEvent e) =>
|
||||
ffi.inputModel.lastMousePos = e.position,
|
||||
onPointerHover: (PointerHoverEvent e) => {
|
||||
if (ffi != null) {ffi.inputModel.lastMousePos = e.position}
|
||||
},
|
||||
child: MouseRegion(
|
||||
child: child,
|
||||
),
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||
|
||||
class DesktopScrollWrapper extends StatelessWidget {
|
||||
final ScrollController scrollController;
|
||||
final Widget child;
|
||||
const DesktopScrollWrapper(
|
||||
{Key? key, required this.scrollController, required this.child})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImprovedScrolling(
|
||||
scrollController: scrollController,
|
||||
enableCustomMouseWheelScrolling: true,
|
||||
// enableKeyboardScrolling: true, // strange behavior on mac
|
||||
customMouseWheelScrollConfig: CustomMouseWheelScrollConfig(
|
||||
scrollDuration: kDefaultScrollDuration,
|
||||
scrollCurve: Curves.linearToEaseOut,
|
||||
mouseWheelTurnsThrottleTimeMs:
|
||||
kDefaultMouseWheelThrottleDuration.inMilliseconds,
|
||||
scrollAmountMultiplier: kDefaultScrollAmountMultiplier),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user