mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-17 02:01:01 +03:00
Compare commits
548 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f02bc9d3e | ||
|
|
5fa8c25e65 | ||
|
|
c44803f5b0 | ||
|
|
4b066b1fba | ||
|
|
dd004f1a2d | ||
|
|
222dbf12cd | ||
|
|
b5d54debce | ||
|
|
08cdf7134d | ||
|
|
be5037bd03 | ||
|
|
f9915df926 | ||
|
|
f96c759cf5 | ||
|
|
8f329ebc1a | ||
|
|
4a3c11e711 | ||
|
|
0dbd3094ec | ||
|
|
40999c3211 | ||
|
|
7c2d62237f | ||
|
|
ef90ab2bd4 | ||
|
|
4f3b821883 | ||
|
|
98b00cdb3d | ||
|
|
8e4127b6a0 | ||
|
|
b1f54acf90 | ||
|
|
39a430f96f | ||
|
|
a9f2e14091 | ||
|
|
77baba3122 | ||
|
|
1c62a28ef3 | ||
|
|
9ed2499666 | ||
|
|
06bc554216 | ||
|
|
090f5b65ac | ||
|
|
49dabd3533 | ||
|
|
7289dbc80f | ||
|
|
72f5184ee0 | ||
|
|
e9c5e0d26b | ||
|
|
03999d900e | ||
|
|
b24551da7b | ||
|
|
bc461fe99b | ||
|
|
25e438a663 | ||
|
|
1f5aeda41d | ||
|
|
9114743577 | ||
|
|
7830a9e9f3 | ||
|
|
5fa8485130 | ||
|
|
ed9cb37283 | ||
|
|
e4b270a581 | ||
|
|
9dd9c45afc | ||
|
|
e163b75407 | ||
|
|
10ff3e6937 | ||
|
|
acae6d6558 | ||
|
|
d025ca1d81 | ||
|
|
e5aa31eb4c | ||
|
|
771cc565ab | ||
|
|
db3bdb16a1 | ||
|
|
c06e1d74b4 | ||
|
|
b544a2889b | ||
|
|
9c45636875 | ||
|
|
827b5f6a4c | ||
|
|
b0791ba183 | ||
|
|
b24b381575 | ||
|
|
0751005073 | ||
|
|
fe06cf77da | ||
|
|
d57cf204c8 | ||
|
|
63e22b7685 | ||
|
|
a02d2bb4ac | ||
|
|
0f7d78c263 | ||
|
|
0e321bd845 | ||
|
|
875b738222 | ||
|
|
b39e851262 | ||
|
|
ec466d459f | ||
|
|
d4a712bb32 | ||
|
|
1c17fddf51 | ||
|
|
3c838e7a92 | ||
|
|
8f44787ba3 | ||
|
|
12e15b5a37 | ||
|
|
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 |
@@ -1,8 +1,16 @@
|
|||||||
[target.x86_64-pc-windows-msvc]
|
[target.x86_64-pc-windows-msvc]
|
||||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||||
[target.i686-pc-windows-msvc]
|
[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")']
|
[target.'cfg(target_os="macos")']
|
||||||
rustflags = [
|
rustflags = [
|
||||||
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
|
"-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
|
- type: input
|
||||||
id: os
|
id: os
|
||||||
attributes:
|
attributes:
|
||||||
label: Operating system(s) on local side and 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 side -> remote side.
|
description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Windows 10 -> osx
|
Windows 10 -> osx
|
||||||
validations:
|
validations:
|
||||||
@@ -35,8 +35,8 @@ body:
|
|||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: RustDesk Version(s) on local side and 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 side -> remote side.
|
description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1.1.9 -> 1.1.8
|
1.1.9 -> 1.1.8
|
||||||
validations:
|
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) {
|
||||||
13
.github/workflows/bridge.yml
vendored
13
.github/workflows/bridge.yml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: "3.16.9"
|
FLUTTER_VERSION: "3.22.3"
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||||
|
|
||||||
@@ -25,6 +25,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install prerequisites
|
- name: Install prerequisites
|
||||||
run: |
|
run: |
|
||||||
@@ -73,12 +75,13 @@ jobs:
|
|||||||
- name: Install flutter rust bridge deps
|
- name: Install flutter rust bridge deps
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
|
||||||
pushd flutter && flutter pub get && popd
|
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
|
- name: Run flutter rust bridge
|
||||||
run: |
|
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
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@master
|
||||||
@@ -89,3 +92,5 @@ jobs:
|
|||||||
./src/bridge_generated.io.rs
|
./src/bridge_generated.io.rs
|
||||||
./flutter/lib/generated_bridge.dart
|
./flutter/lib/generated_bridge.dart
|
||||||
./flutter/lib/generated_bridge.freezed.dart
|
./flutter/lib/generated_bridge.freezed.dart
|
||||||
|
./flutter/macos/Runner/bridge_generated.h
|
||||||
|
./flutter/ios/Runner/bridge_generated.h
|
||||||
|
|||||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -4,9 +4,9 @@ env:
|
|||||||
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
||||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2023.10.19
|
# vcpkg version: 2024.11.16
|
||||||
# for multiarch gcc compatibility
|
# for multiarch gcc compatibility
|
||||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -45,6 +45,8 @@ jobs:
|
|||||||
# steps:
|
# steps:
|
||||||
# - name: Checkout source code
|
# - name: Checkout source code
|
||||||
# uses: actions/checkout@v3
|
# uses: actions/checkout@v3
|
||||||
|
# with:
|
||||||
|
# submodules: recursive
|
||||||
|
|
||||||
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
|
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
|
||||||
# uses: actions-rs/toolchain@v1
|
# uses: actions-rs/toolchain@v1
|
||||||
@@ -92,6 +94,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install prerequisites
|
- name: Install prerequisites
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -112,6 +116,8 @@ jobs:
|
|||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
|
libva-dev \
|
||||||
|
libvdpau-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
libxcb-shape0-dev \
|
libxcb-shape0-dev \
|
||||||
libxcb-xfixes0-dev \
|
libxcb-xfixes0-dev \
|
||||||
|
|||||||
7
.github/workflows/fdroid.yml
vendored
7
.github/workflows/fdroid.yml
vendored
@@ -19,7 +19,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate RustDesk version file
|
- name: Generate RustDesk version file
|
||||||
run: |
|
run: |
|
||||||
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
|
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)"
|
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 "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
||||||
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
||||||
|
|||||||
814
.github/workflows/flutter-build.yml
vendored
814
.github/workflows/flutter-build.yml
vendored
File diff suppressed because it is too large
Load Diff
18
.github/workflows/playground.yml
vendored
18
.github/workflows/playground.yml
vendored
@@ -16,9 +16,9 @@ env:
|
|||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "nightly"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.06.15
|
# vcpkg version: 2024.11.16
|
||||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
|
||||||
VERSION: "1.2.7"
|
VERSION: "1.3.7"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -90,7 +90,8 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.job.ref }}
|
ref: ${{ matrix.job.ref }}
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Import the codesign cert
|
- name: Import the codesign cert
|
||||||
if: env.MACOS_P12_BASE64 != null
|
if: env.MACOS_P12_BASE64 != null
|
||||||
uses: apple-actions/import-codesign-certs@v1
|
uses: apple-actions/import-codesign-certs@v1
|
||||||
@@ -149,7 +150,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
|
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"
|
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" --locked
|
||||||
# below works for mac to make buildable on 3.13.9
|
# 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/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
|
||||||
pushd flutter && flutter pub get && popd
|
pushd flutter && flutter pub get && popd
|
||||||
@@ -250,6 +251,7 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.job.ref }}
|
ref: ${{ matrix.job.ref }}
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -262,7 +264,7 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
g++-multilib \
|
g++-multilib \
|
||||||
libappindicator3-dev \
|
libayatana-appindicator3-dev\
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libc6-dev \
|
libc6-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
@@ -302,7 +304,7 @@ jobs:
|
|||||||
- name: Install flutter rust bridge deps
|
- name: Install flutter rust bridge deps
|
||||||
run: |
|
run: |
|
||||||
git config --global core.longpaths true
|
git config --global core.longpaths true
|
||||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
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
|
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/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
|
||||||
pushd flutter ; flutter pub get ; popd
|
pushd flutter ; flutter pub get ; popd
|
||||||
@@ -347,7 +349,7 @@ jobs:
|
|||||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.job.target }}
|
rustup target add ${{ matrix.job.target }}
|
||||||
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
|
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
|
||||||
case ${{ matrix.job.target }} in
|
case ${{ matrix.job.target }} in
|
||||||
aarch64-linux-android)
|
aarch64-linux-android)
|
||||||
./flutter/ndk_arm64.sh
|
./flutter/ndk_arm64.sh
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -54,3 +54,4 @@ examples/**/target/
|
|||||||
vcpkg_installed
|
vcpkg_installed
|
||||||
flutter/lib/generated_plugin_registrant.dart
|
flutter/lib/generated_plugin_registrant.dart
|
||||||
libsciter.dylib
|
libsciter.dylib
|
||||||
|
flutter/web/
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "libs/hbb_common"]
|
||||||
|
path = libs/hbb_common
|
||||||
|
url = https://github.com/rustdesk/hbb_common
|
||||||
464
Cargo.lock
generated
464
Cargo.lock
generated
@@ -123,7 +123,7 @@ checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "android-wakelock"
|
name = "android-wakelock"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/21pages/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90"
|
source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
"log",
|
"log",
|
||||||
@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "arboard"
|
name = "arboard"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
source = "git+https://github.com/rustdesk-org/arboard#27b4e503caa70ec6306e5270461429f2cf907ad6"
|
source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clipboard-win",
|
"clipboard-win",
|
||||||
"core-graphics 0.23.2",
|
"core-graphics 0.23.2",
|
||||||
@@ -234,24 +234,13 @@ dependencies = [
|
|||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"resvg",
|
"serde 1.0.203",
|
||||||
|
"serde_derive",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
"wl-clipboard-rs",
|
"wl-clipboard-rs",
|
||||||
"x11rb 0.13.1",
|
"x11rb 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayref"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arrayvec"
|
|
||||||
version = "0.7.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -734,9 +723,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.16.1"
|
version = "1.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
|
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -746,9 +735,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.6.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde 1.0.203",
|
"serde 1.0.203",
|
||||||
]
|
]
|
||||||
@@ -871,6 +860,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.38"
|
version = "0.4.38"
|
||||||
@@ -898,6 +893,20 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cidre"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
|
||||||
|
dependencies = [
|
||||||
|
"cidre-macros",
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cidre-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -987,11 +996,14 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-master"
|
name = "clipboard-master"
|
||||||
version = "4.0.0-beta.6"
|
version = "4.0.0-beta.6"
|
||||||
source = "git+https://github.com/rustdesk-org/clipboard-master#5268c7b3d7728699566ad863da0911f249706f8c"
|
source = "git+https://github.com/rustdesk-org/clipboard-master#4fb62e5b62fb6350d82b571ec7ba94b3cd466695"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"objc",
|
"objc",
|
||||||
"objc-foundation",
|
"objc-foundation",
|
||||||
"objc_id",
|
"objc_id",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"wayland-protocols-wlr",
|
||||||
"windows-win",
|
"windows-win",
|
||||||
"wl-clipboard-rs",
|
"wl-clipboard-rs",
|
||||||
"x11-clipboard 0.9.2",
|
"x11-clipboard 0.9.2",
|
||||||
@@ -1000,9 +1012,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.3.1"
|
version = "5.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad"
|
checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"error-code",
|
"error-code",
|
||||||
]
|
]
|
||||||
@@ -1278,10 +1290,10 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cpal"
|
name = "cpal"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6b374bcaed076750ca8fce6da518ab39b882e14a"
|
||||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alsa",
|
"alsa",
|
||||||
|
"cidre",
|
||||||
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"coreaudio-rs",
|
"coreaudio-rs",
|
||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
@@ -1526,12 +1538,6 @@ dependencies = [
|
|||||||
"dasp_sample",
|
"dasp_sample",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "data-url"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbus"
|
name = "dbus"
|
||||||
version = "0.9.7"
|
version = "0.9.7"
|
||||||
@@ -1575,6 +1581,16 @@ dependencies = [
|
|||||||
"windows 0.32.0",
|
"windows 0.32.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "default_net"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/rustdesk-org/default_net#a831d47bcacb4615b394968287697924a8f62be1"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"regex",
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@@ -2081,12 +2097,6 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "float-cmp"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -2143,29 +2153,6 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fontconfig-parser"
|
|
||||||
version = "0.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d"
|
|
||||||
dependencies = [
|
|
||||||
"roxmltree 0.19.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fontdb"
|
|
||||||
version = "0.18.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
|
|
||||||
dependencies = [
|
|
||||||
"fontconfig-parser",
|
|
||||||
"log",
|
|
||||||
"memmap2",
|
|
||||||
"slotmap",
|
|
||||||
"tinyvec",
|
|
||||||
"ttf-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2274,9 +2261,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@@ -2284,9 +2271,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
@@ -2301,9 +2288,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-lite"
|
name = "futures-lite"
|
||||||
@@ -2335,9 +2322,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.86",
|
"proc-macro2 1.0.86",
|
||||||
"quote 1.0.36",
|
"quote 1.0.36",
|
||||||
@@ -2346,21 +2333,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -2924,6 +2911,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"confy",
|
"confy",
|
||||||
|
"default_net",
|
||||||
"directories-next",
|
"directories-next",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"dlopen",
|
"dlopen",
|
||||||
@@ -2948,6 +2936,7 @@ dependencies = [
|
|||||||
"serde 1.0.203",
|
"serde 1.0.203",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json 1.0.118",
|
"serde_json 1.0.118",
|
||||||
|
"sha2",
|
||||||
"socket2 0.3.19",
|
"socket2 0.3.19",
|
||||||
"sodiumoxide",
|
"sodiumoxide",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
@@ -3087,8 +3076,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hwcodec"
|
name = "hwcodec"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
source = "git+https://github.com/21pages/hwcodec#89879f2f02c6f74e88a4a43744a1153aec5b7e7f"
|
source = "git+https://github.com/rustdesk-org/hwcodec#c4d6b1c5c4ddc7548868306004cf5d4eb614a36f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -3213,16 +3202,10 @@ dependencies = [
|
|||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "imagesize"
|
|
||||||
version = "0.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "impersonate_system"
|
name = "impersonate_system"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/21pages/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167"
|
source = "git+https://github.com/rustdesk-org/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
@@ -3462,16 +3445,6 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kurbo"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c"
|
|
||||||
dependencies = [
|
|
||||||
"arrayvec",
|
|
||||||
"smallvec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -3558,7 +3531,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.52.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3733,7 +3706,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "machine-uid"
|
name = "machine-uid"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
source = "git+https://github.com/21pages/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542"
|
source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -3777,15 +3750,6 @@ version = "2.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memmap2"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "memoffset"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
@@ -3847,14 +3811,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mouce"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "git+https://github.com/rustdesk-org/mouce.git#177625a395cd8fa73964714d0039535cb9b47893"
|
|
||||||
dependencies = [
|
|
||||||
"glob",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.13.5"
|
version = "0.13.5"
|
||||||
@@ -4043,11 +3999,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.1.1",
|
||||||
"libc",
|
"libc",
|
||||||
"memoffset 0.9.1",
|
"memoffset 0.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"cfg_aliases 0.2.1",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@@ -4207,7 +4175,7 @@ version = "0.7.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 2.0.2",
|
"proc-macro-crate 1.3.1",
|
||||||
"proc-macro2 1.0.86",
|
"proc-macro2 1.0.86",
|
||||||
"quote 1.0.36",
|
"quote 1.0.36",
|
||||||
"syn 2.0.68",
|
"syn 2.0.68",
|
||||||
@@ -4426,9 +4394,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.64"
|
version = "0.10.68"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
@@ -4458,9 +4426,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.102"
|
version = "0.9.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -4732,15 +4700,9 @@ version = "0.7.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
|
checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"siphasher 0.2.3",
|
"siphasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pico-args"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.5"
|
version = "1.1.5"
|
||||||
@@ -5065,6 +5027,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "0.6.13"
|
version = "0.6.13"
|
||||||
@@ -5260,7 +5231,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rdev"
|
name = "rdev"
|
||||||
version = "0.5.0-2"
|
version = "0.5.0-2"
|
||||||
source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488"
|
source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa 0.24.1",
|
"cocoa 0.24.1",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
@@ -5414,31 +5385,6 @@ dependencies = [
|
|||||||
"winreg 0.50.0",
|
"winreg 0.50.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "resvg"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051"
|
|
||||||
dependencies = [
|
|
||||||
"gif",
|
|
||||||
"jpeg-decoder",
|
|
||||||
"log",
|
|
||||||
"pico-args",
|
|
||||||
"rgb",
|
|
||||||
"svgtypes",
|
|
||||||
"tiny-skia",
|
|
||||||
"usvg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rgb"
|
|
||||||
version = "0.8.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.8"
|
version = "0.17.8"
|
||||||
@@ -5463,18 +5409,6 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "roxmltree"
|
|
||||||
version = "0.19.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "roxmltree"
|
|
||||||
version = "0.20.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpassword"
|
name = "rpassword"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@@ -5544,7 +5478,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-pulsectl"
|
name = "rust-pulsectl"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
source = "git+https://github.com/open-trade/pulsectl#5e68f4c2b7c644fa321984688602d71e8ad0bba3"
|
source = "git+https://github.com/rustdesk-org/pulsectl#aa34dde499aa912a3abc5289cc0b547bd07dd6e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libpulse-binding",
|
"libpulse-binding",
|
||||||
]
|
]
|
||||||
@@ -5572,7 +5506,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.7"
|
version = "1.3.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -5604,6 +5538,7 @@ dependencies = [
|
|||||||
"flutter_rust_bridge",
|
"flutter_rust_bridge",
|
||||||
"fon",
|
"fon",
|
||||||
"fruitbasket",
|
"fruitbasket",
|
||||||
|
"gtk",
|
||||||
"hbb_common",
|
"hbb_common",
|
||||||
"hex",
|
"hex",
|
||||||
"hound",
|
"hound",
|
||||||
@@ -5618,7 +5553,7 @@ dependencies = [
|
|||||||
"libpulse-simple-binding",
|
"libpulse-simple-binding",
|
||||||
"mac_address",
|
"mac_address",
|
||||||
"magnum-opus",
|
"magnum-opus",
|
||||||
"mouce",
|
"nix 0.29.0",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"objc",
|
"objc",
|
||||||
"objc_id",
|
"objc_id",
|
||||||
@@ -5650,6 +5585,7 @@ dependencies = [
|
|||||||
"system_shutdown",
|
"system_shutdown",
|
||||||
"tao",
|
"tao",
|
||||||
"tauri-winrt-notification",
|
"tauri-winrt-notification",
|
||||||
|
"termios",
|
||||||
"totp-rs",
|
"totp-rs",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"url",
|
"url",
|
||||||
@@ -5670,7 +5606,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.2.7"
|
version = "1.3.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -5853,22 +5789,6 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustybuzz"
|
|
||||||
version = "0.14.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.6.0",
|
|
||||||
"bytemuck",
|
|
||||||
"smallvec",
|
|
||||||
"ttf-parser",
|
|
||||||
"unicode-bidi-mirroring",
|
|
||||||
"unicode-ccc",
|
|
||||||
"unicode-properties",
|
|
||||||
"unicode-script",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -5905,7 +5825,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sciter-rs"
|
name = "sciter-rs"
|
||||||
version = "0.5.57"
|
version = "0.5.57"
|
||||||
source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#fab913b7c2e779b05c249b0c5de5a08759b2c15d"
|
source = "git+https://github.com/rustdesk-org/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -6159,27 +6079,12 @@ version = "0.3.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "simplecss"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
|
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "siphasher"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -6189,15 +6094,6 @@ dependencies = [
|
|||||||
"autocfg 1.3.0",
|
"autocfg 1.3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slotmap"
|
|
||||||
version = "1.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
|
|
||||||
dependencies = [
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.2"
|
version = "1.13.2"
|
||||||
@@ -6268,15 +6164,6 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strict-num"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
|
||||||
dependencies = [
|
|
||||||
"float-cmp",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -6338,16 +6225,6 @@ version = "2.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "svgtypes"
|
|
||||||
version = "0.15.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c"
|
|
||||||
dependencies = [
|
|
||||||
"kurbo",
|
|
||||||
"siphasher 1.0.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "0.15.44"
|
version = "0.15.44"
|
||||||
@@ -6399,7 +6276,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sysinfo"
|
name = "sysinfo"
|
||||||
version = "0.29.10"
|
version = "0.29.10"
|
||||||
source = "git+https://github.com/rustdesk-org/sysinfo#f45dcc6510d48c3a1401c5a33eedccc8899f67b2"
|
source = "git+https://github.com/rustdesk-org/sysinfo?branch=rlim_max#90b1705d909a4902dbbbdea37ee64db17841077d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -6593,11 +6470,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tfc"
|
name = "tfc"
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
source = "git+https://github.com/rustdesk-org/The-Fat-Controller#9dd86151525fd010dc93f6bc9b6aedd1a75cc342"
|
source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"core-graphics 0.22.3",
|
"core-graphics 0.23.2",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
"x11 2.19.0",
|
"x11 2.19.0",
|
||||||
@@ -6687,32 +6564,6 @@ dependencies = [
|
|||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tiny-skia"
|
|
||||||
version = "0.11.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"arrayvec",
|
|
||||||
"bytemuck",
|
|
||||||
"cfg-if 1.0.0",
|
|
||||||
"log",
|
|
||||||
"png",
|
|
||||||
"tiny-skia-path",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tiny-skia-path"
|
|
||||||
version = "0.11.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
|
||||||
dependencies = [
|
|
||||||
"arrayref",
|
|
||||||
"bytemuck",
|
|
||||||
"strict-num",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@@ -7004,12 +6855,6 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ttf-parser"
|
|
||||||
version = "0.21.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -7082,18 +6927,6 @@ version = "0.3.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-bidi-mirroring"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ccc"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
@@ -7109,30 +6942,12 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-properties"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-script"
|
|
||||||
version = "0.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-vo"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -7195,33 +7010,6 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "usvg"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"data-url",
|
|
||||||
"flate2",
|
|
||||||
"fontdb",
|
|
||||||
"imagesize",
|
|
||||||
"kurbo",
|
|
||||||
"log",
|
|
||||||
"pico-args",
|
|
||||||
"roxmltree 0.20.0",
|
|
||||||
"rustybuzz",
|
|
||||||
"simplecss",
|
|
||||||
"siphasher 1.0.1",
|
|
||||||
"strict-num",
|
|
||||||
"svgtypes",
|
|
||||||
"tiny-skia-path",
|
|
||||||
"unicode-bidi",
|
|
||||||
"unicode-script",
|
|
||||||
"unicode-vo",
|
|
||||||
"xmlwriter",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf16string"
|
name = "utf16string"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -7309,7 +7097,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "wallpaper"
|
name = "wallpaper"
|
||||||
version = "3.2.0"
|
version = "3.2.0"
|
||||||
source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
|
source = "git+https://github.com/rustdesk-org/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"enquote",
|
"enquote",
|
||||||
@@ -7414,9 +7202,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-backend"
|
name = "wayland-backend"
|
||||||
version = "0.3.4"
|
version = "0.3.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07"
|
checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
@@ -7428,9 +7216,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-client"
|
name = "wayland-client"
|
||||||
version = "0.31.3"
|
version = "0.31.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133"
|
checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"rustix 0.38.34",
|
"rustix 0.38.34",
|
||||||
@@ -7440,9 +7228,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols"
|
name = "wayland-protocols"
|
||||||
version = "0.32.1"
|
version = "0.32.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83d0f1056570486e26a3773ec633885124d79ae03827de05ba6c85f79904026c"
|
checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
@@ -7452,9 +7240,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols-wlr"
|
name = "wayland-protocols-wlr"
|
||||||
version = "0.3.1"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7dab47671043d9f5397035975fe1cac639e5bca5cc0b3c32d09f01612e34d24"
|
checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
@@ -7465,20 +7253,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-scanner"
|
name = "wayland-scanner"
|
||||||
version = "0.31.2"
|
version = "0.31.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565"
|
checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.86",
|
"proc-macro2 1.0.86",
|
||||||
"quick-xml 0.31.0",
|
"quick-xml 0.34.0",
|
||||||
"quote 1.0.36",
|
"quote 1.0.36",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-sys"
|
name = "wayland-sys"
|
||||||
version = "0.31.2"
|
version = "0.31.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12"
|
checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dlib",
|
"dlib",
|
||||||
"log",
|
"log",
|
||||||
@@ -7498,7 +7286,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "webm"
|
name = "webm"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
|
source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webm-sys",
|
"webm-sys",
|
||||||
]
|
]
|
||||||
@@ -7506,7 +7294,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "webm-sys"
|
name = "webm-sys"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
|
source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
@@ -8220,12 +8008,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xmlwriter"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zbus"
|
name = "zbus"
|
||||||
version = "3.15.2"
|
version = "3.15.2"
|
||||||
|
|||||||
28
Cargo.toml
28
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.7"
|
version = "1.3.7"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -16,6 +16,10 @@ crate-type = ["cdylib", "staticlib", "rlib"]
|
|||||||
name = "naming"
|
name = "naming"
|
||||||
path = "src/naming.rs"
|
path = "src/naming.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "service"
|
||||||
|
path = "src/service.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
inline = []
|
inline = []
|
||||||
cli = []
|
cli = []
|
||||||
@@ -36,6 +40,7 @@ unix-file-copy-paste = [
|
|||||||
"dep:once_cell",
|
"dep:once_cell",
|
||||||
"clipboard/unix-file-copy-paste",
|
"clipboard/unix-file-copy-paste",
|
||||||
]
|
]
|
||||||
|
screencapturekit = ["cpal/screencapturekit"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -78,19 +83,20 @@ zip = "0.6"
|
|||||||
shutdown_hooks = "0.1"
|
shutdown_hooks = "0.1"
|
||||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
|
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||||
cpal = "0.15"
|
# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
|
||||||
|
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
|
||||||
ringbuf = "0.3"
|
ringbuf = "0.3"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
mac_address = "1.1"
|
mac_address = "1.1"
|
||||||
sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" }
|
sciter-rs = { git = "https://github.com/rustdesk-org/rust-sciter", branch = "dyn" }
|
||||||
sys-locale = "0.3"
|
sys-locale = "0.3"
|
||||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||||
clipboard = { path = "libs/clipboard" }
|
clipboard = { path = "libs/clipboard" }
|
||||||
ctrlc = "3.2"
|
ctrlc = "3.2"
|
||||||
# arboard = { version = "3.4.0", 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", "image-data"] }
|
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
|
||||||
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||||
|
|
||||||
system_shutdown = "4.0"
|
system_shutdown = "4.0"
|
||||||
@@ -114,7 +120,7 @@ winapi = { version = "0.3", features = [
|
|||||||
winreg = "0.11"
|
winreg = "0.11"
|
||||||
windows-service = "0.6"
|
windows-service = "0.6"
|
||||||
virtual_display = { path = "libs/virtual_display" }
|
virtual_display = { path = "libs/virtual_display" }
|
||||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
|
||||||
shared_memory = "0.12"
|
shared_memory = "0.12"
|
||||||
tauri-winrt-notification = "0.1.2"
|
tauri-winrt-notification = "0.1.2"
|
||||||
runas = "1.2"
|
runas = "1.2"
|
||||||
@@ -138,7 +144,7 @@ image = "0.24"
|
|||||||
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
|
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
[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]
|
[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
|
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||||
@@ -150,9 +156,8 @@ reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocki
|
|||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||||
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
rust-pulsectl = { git = "https://github.com/rustdesk-org/pulsectl" }
|
||||||
async-process = "1.7"
|
async-process = "1.7"
|
||||||
mouce = { git="https://github.com/rustdesk-org/mouce.git" }
|
|
||||||
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||||
dbus = "0.9"
|
dbus = "0.9"
|
||||||
dbus-crossroads = "0.5"
|
dbus-crossroads = "0.5"
|
||||||
@@ -162,11 +167,14 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/
|
|||||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||||
percent-encoding = {version = "2.3", optional = true}
|
percent-encoding = {version = "2.3", optional = true}
|
||||||
once_cell = {version = "1.18", 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]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.13"
|
android_logger = "0.13"
|
||||||
jni = "0.21"
|
jni = "0.21"
|
||||||
android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
|
android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
||||||
|
|||||||
11
Dockerfile
11
Dockerfile
@@ -2,6 +2,7 @@ FROM debian:bullseye-slim
|
|||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV VCPKG_FORCE_SYSTEM_BINARIES=1
|
||||||
RUN apt update -y && \
|
RUN apt update -y && \
|
||||||
apt install --yes --no-install-recommends \
|
apt install --yes --no-install-recommends \
|
||||||
g++ \
|
g++ \
|
||||||
@@ -21,7 +22,8 @@ RUN apt update -y && \
|
|||||||
libpam0g-dev \
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
make \
|
make \
|
||||||
cmake \
|
wget \
|
||||||
|
libssl-dev \
|
||||||
unzip \
|
unzip \
|
||||||
zip \
|
zip \
|
||||||
sudo \
|
sudo \
|
||||||
@@ -31,6 +33,13 @@ RUN apt update -y && \
|
|||||||
ninja-build && \
|
ninja-build && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN wget https://github.com/Kitware/CMake/releases/download/v3.30.6/cmake-3.30.6.tar.gz --no-check-certificate && \
|
||||||
|
tar xzf cmake-3.30.6.tar.gz && \
|
||||||
|
cd cmake-3.30.6 && \
|
||||||
|
./configure --prefix=/usr/local && \
|
||||||
|
make && \
|
||||||
|
make install
|
||||||
|
|
||||||
RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \
|
RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \
|
||||||
/vcpkg/bootstrap-vcpkg.sh -disableMetrics && \
|
/vcpkg/bootstrap-vcpkg.sh -disableMetrics && \
|
||||||
/vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom
|
/vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
<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="#raw-steps-to-build">Build</a> •
|
||||||
<a href="#how-to-build-with-docker">Docker</a> •
|
<a href="#how-to-build-with-docker">Docker</a> •
|
||||||
<a href="#file-structure">Structure</a> •
|
<a href="#file-structure">Structure</a> •
|
||||||
@@ -25,9 +25,12 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
|
|||||||
|
|
||||||
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||||
alt="Get it on F-Droid"
|
alt="Get it on F-Droid"
|
||||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||||
|
[<img src="https://flathub.org/api/badge?svg&locale=en"
|
||||||
|
alt="Get it on Flathub"
|
||||||
|
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -59,19 +62,19 @@ Please download Sciter dynamic library yourself.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```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)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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)
|
### Arch (Manjaro)
|
||||||
@@ -171,3 +174,4 @@ Please ensure that you are running these commands from the root of the RustDesk
|
|||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.7
|
version: 1.3.7
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
arch:
|
arch:
|
||||||
@@ -47,9 +47,9 @@ AppDir:
|
|||||||
- libasound2
|
- libasound2
|
||||||
- libsystemd0
|
- libsystemd0
|
||||||
- curl
|
- curl
|
||||||
|
- libva2
|
||||||
- libva-drm2
|
- libva-drm2
|
||||||
- libva-x11-2
|
- libva-x11-2
|
||||||
- libvdpau1
|
|
||||||
- libgstreamer-plugins-base1.0-0
|
- libgstreamer-plugins-base1.0-0
|
||||||
- gstreamer1.0-pipewire
|
- gstreamer1.0-pipewire
|
||||||
- libwayland-client0
|
- libwayland-client0
|
||||||
@@ -77,7 +77,7 @@ AppDir:
|
|||||||
env:
|
env:
|
||||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-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
|
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
|
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/share/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_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
|
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:
|
test:
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.7
|
version: 1.3.7
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
arch:
|
arch:
|
||||||
@@ -37,6 +37,9 @@ AppDir:
|
|||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
||||||
universe multiverse
|
universe multiverse
|
||||||
include:
|
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
|
- libc6:amd64
|
||||||
- libgtk-3-0
|
- libgtk-3-0
|
||||||
- libxcb-randr0
|
- libxcb-randr0
|
||||||
@@ -47,9 +50,9 @@ AppDir:
|
|||||||
- libasound2
|
- libasound2
|
||||||
- libsystemd0
|
- libsystemd0
|
||||||
- curl
|
- curl
|
||||||
|
- libva2
|
||||||
- libva-drm2
|
- libva-drm2
|
||||||
- libva-x11-2
|
- libva-x11-2
|
||||||
- libvdpau1
|
|
||||||
- libgstreamer-plugins-base1.0-0
|
- libgstreamer-plugins-base1.0-0
|
||||||
- gstreamer1.0-pipewire
|
- gstreamer1.0-pipewire
|
||||||
- libwayland-client0
|
- libwayland-client0
|
||||||
@@ -77,7 +80,7 @@ AppDir:
|
|||||||
env:
|
env:
|
||||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$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
|
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
|
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/share/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_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
|
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:
|
test:
|
||||||
|
|||||||
64
build.py
64
build.py
@@ -9,6 +9,7 @@ import shutil
|
|||||||
import hashlib
|
import hashlib
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
windows = platform.platform().startswith('Windows')
|
windows = platform.platform().startswith('Windows')
|
||||||
osx = platform.platform().startswith(
|
osx = platform.platform().startswith(
|
||||||
@@ -31,6 +32,11 @@ def get_deb_arch() -> str:
|
|||||||
return "amd64"
|
return "amd64"
|
||||||
return custom_arch
|
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):
|
def system2(cmd):
|
||||||
exit_code = os.system(cmd)
|
exit_code = os.system(cmd)
|
||||||
@@ -106,7 +112,7 @@ def make_parser():
|
|||||||
'--hwcodec',
|
'--hwcodec',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Enable feature hwcodec' + (
|
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(
|
parser.add_argument(
|
||||||
'--vram',
|
'--vram',
|
||||||
@@ -138,6 +144,12 @@ def make_parser():
|
|||||||
"--package",
|
"--package",
|
||||||
type=str
|
type=str
|
||||||
)
|
)
|
||||||
|
if osx:
|
||||||
|
parser.add_argument(
|
||||||
|
'--screencapturekit',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable feature screencapturekit'
|
||||||
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -269,6 +281,9 @@ def get_features(args):
|
|||||||
features.append('flutter')
|
features.append('flutter')
|
||||||
if args.unix_file_copy_paste:
|
if args.unix_file_copy_paste:
|
||||||
features.append('unix-file-copy-paste')
|
features.append('unix-file-copy-paste')
|
||||||
|
if osx:
|
||||||
|
if args.screencapturekit:
|
||||||
|
features.append('screencapturekit')
|
||||||
print("features:", features)
|
print("features:", features)
|
||||||
return features
|
return features
|
||||||
|
|
||||||
@@ -278,14 +293,17 @@ def generate_control_file(version):
|
|||||||
system2('/bin/rm -rf %s' % control_file_path)
|
system2('/bin/rm -rf %s' % control_file_path)
|
||||||
|
|
||||||
content = """Package: rustdesk
|
content = """Package: rustdesk
|
||||||
|
Section: net
|
||||||
|
Priority: optional
|
||||||
Version: %s
|
Version: %s
|
||||||
Architecture: %s
|
Architecture: %s
|
||||||
Maintainer: rustdesk <info@rustdesk.com>
|
Maintainer: rustdesk <info@rustdesk.com>
|
||||||
Homepage: https://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.
|
Description: A remote control software.
|
||||||
|
|
||||||
""" % (version, get_deb_arch())
|
""" % (version, get_deb_arch(), get_deb_extra_depends())
|
||||||
file = open(control_file_path, "w")
|
file = open(control_file_path, "w")
|
||||||
file.write(content)
|
file.write(content)
|
||||||
file.close()
|
file.close()
|
||||||
@@ -304,7 +322,7 @@ def build_flutter_deb(version, features):
|
|||||||
os.chdir('flutter')
|
os.chdir('flutter')
|
||||||
system2('flutter build linux --release')
|
system2('flutter build linux --release')
|
||||||
system2('mkdir -p tmpdeb/usr/bin/')
|
system2('mkdir -p tmpdeb/usr/bin/')
|
||||||
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
||||||
system2('mkdir -p tmpdeb/etc/rustdesk/')
|
system2('mkdir -p tmpdeb/etc/rustdesk/')
|
||||||
system2('mkdir -p tmpdeb/etc/pam.d/')
|
system2('mkdir -p tmpdeb/etc/pam.d/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
@@ -314,7 +332,7 @@ def build_flutter_deb(version, features):
|
|||||||
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
|
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
|
||||||
system2('rm tmpdeb/usr/bin/rustdesk || true')
|
system2('rm tmpdeb/usr/bin/rustdesk || true')
|
||||||
system2(
|
system2(
|
||||||
f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/')
|
f'cp -r {flutter_build_dir}/* tmpdeb/usr/share/rustdesk/')
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
system2(
|
system2(
|
||||||
@@ -325,8 +343,6 @@ def build_flutter_deb(version, features):
|
|||||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
'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(
|
system2(
|
||||||
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||||
system2(
|
system2(
|
||||||
@@ -339,7 +355,7 @@ def build_flutter_deb(version, features):
|
|||||||
system2('mkdir -p tmpdeb/DEBIAN')
|
system2('mkdir -p tmpdeb/DEBIAN')
|
||||||
generate_control_file(version)
|
generate_control_file(version)
|
||||||
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
|
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
|
||||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
md5_file_folder("tmpdeb/")
|
||||||
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
|
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
|
||||||
|
|
||||||
system2('/bin/rm -rf tmpdeb/')
|
system2('/bin/rm -rf tmpdeb/')
|
||||||
@@ -351,7 +367,7 @@ def build_flutter_deb(version, features):
|
|||||||
def build_deb_from_folder(version, binary_folder):
|
def build_deb_from_folder(version, binary_folder):
|
||||||
os.chdir('flutter')
|
os.chdir('flutter')
|
||||||
system2('mkdir -p tmpdeb/usr/bin/')
|
system2('mkdir -p tmpdeb/usr/bin/')
|
||||||
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
||||||
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
|
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
|
||||||
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
|
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
|
||||||
@@ -359,7 +375,7 @@ def build_deb_from_folder(version, binary_folder):
|
|||||||
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
|
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
|
||||||
system2('rm tmpdeb/usr/bin/rustdesk || true')
|
system2('rm tmpdeb/usr/bin/rustdesk || true')
|
||||||
system2(
|
system2(
|
||||||
f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/')
|
f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/')
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||||
system2(
|
system2(
|
||||||
@@ -370,15 +386,13 @@ def build_deb_from_folder(version, binary_folder):
|
|||||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
'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(
|
system2(
|
||||||
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
||||||
|
|
||||||
system2('mkdir -p tmpdeb/DEBIAN')
|
system2('mkdir -p tmpdeb/DEBIAN')
|
||||||
generate_control_file(version)
|
generate_control_file(version)
|
||||||
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
|
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
|
||||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
md5_file_folder("tmpdeb/")
|
||||||
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
|
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
|
||||||
|
|
||||||
system2('/bin/rm -rf tmpdeb/')
|
system2('/bin/rm -rf tmpdeb/')
|
||||||
@@ -391,12 +405,13 @@ def build_flutter_dmg(version, features):
|
|||||||
if not skip_cargo:
|
if not skip_cargo:
|
||||||
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
|
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
|
||||||
system2(
|
system2(
|
||||||
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
|
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release')
|
||||||
# copy dylib
|
# copy dylib
|
||||||
system2(
|
system2(
|
||||||
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
||||||
os.chdir('flutter')
|
os.chdir('flutter')
|
||||||
system2('flutter build macos --release')
|
system2('flutter build macos --release')
|
||||||
|
system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
|
||||||
'''
|
'''
|
||||||
system2(
|
system2(
|
||||||
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
|
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
|
||||||
@@ -608,21 +623,24 @@ def main():
|
|||||||
os.system('mkdir -p tmpdeb/etc/pam.d/')
|
os.system('mkdir -p tmpdeb/etc/pam.d/')
|
||||||
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
||||||
system2('strip tmpdeb/usr/bin/rustdesk')
|
system2('strip tmpdeb/usr/bin/rustdesk')
|
||||||
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
|
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
||||||
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
|
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
|
||||||
system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
|
system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/')
|
||||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
md5_file_folder("tmpdeb/")
|
||||||
md5_file('etc/rustdesk/startwm.sh')
|
|
||||||
md5_file('etc/X11/rustdesk/xorg.conf')
|
|
||||||
md5_file('etc/pam.d/rustdesk')
|
|
||||||
md5_file('usr/lib/rustdesk/libsciter-gtk.so')
|
|
||||||
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
|
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
|
||||||
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
|
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
|
||||||
|
|
||||||
|
|
||||||
def md5_file(fn):
|
def md5_file(fn):
|
||||||
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
|
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
|
||||||
system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
|
system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
|
||||||
|
|
||||||
|
def md5_file_folder(base_dir):
|
||||||
|
base_path = Path(base_dir)
|
||||||
|
for file in base_path.rglob('*'):
|
||||||
|
if file.is_file() and 'DEBIAN' not in file.parts:
|
||||||
|
relative_path = file.relative_to(base_path)
|
||||||
|
md5_file(str(relative_path))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
9
build.rs
9
build.rs
@@ -1,7 +1,7 @@
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn build_windows() {
|
fn build_windows() {
|
||||||
let file = "src/platform/windows.cc";
|
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");
|
cc::Build::new().file(file).file(file2).compile("windows");
|
||||||
println!("cargo:rustc-link-lib=WtsApi32");
|
println!("cargo:rustc-link-lib=WtsApi32");
|
||||||
println!("cargo:rerun-if-changed={}", file);
|
println!("cargo:rerun-if-changed={}", file);
|
||||||
@@ -61,7 +61,11 @@ fn install_android_deps() {
|
|||||||
let target = format!("{}-android", target_arch);
|
let target = format!("{}-android", target_arch);
|
||||||
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
|
let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap();
|
||||||
let mut path: std::path::PathBuf = vcpkg_root.into();
|
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);
|
path.push(target);
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -72,7 +76,6 @@ fn install_android_deps() {
|
|||||||
);
|
);
|
||||||
println!("cargo:rustc-link-lib=ndk_compat");
|
println!("cargo:rustc-link-lib=ndk_compat");
|
||||||
println!("cargo:rustc-link-lib=oboe");
|
println!("cargo:rustc-link-lib=oboe");
|
||||||
println!("cargo:rustc-link-lib=oboe_wrapper");
|
|
||||||
println!("cargo:rustc-link-lib=c++");
|
println!("cargo:rustc-link-lib=c++");
|
||||||
println!("cargo:rustc-link-lib=OpenSLES");
|
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) 进行交流。
|
||||||
@@ -9,12 +9,12 @@
|
|||||||
<b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b>
|
<b>README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.</b>
|
||||||
</p>
|
</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)
|
[](https://ko-fi.com/I2I04VU09)
|
||||||
|
|
||||||
Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [스스로 설정](https://rustdesk.com/server)하는 것도, [스스로 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
|
Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스트탑 소프트웨어입니다. 자신의 데이터를 완전히 컨트롤할 수 있고, 보안의 염려도 없습니다. 우리의 rendezvous/relay 서버를 사용해도, [직접 설정](https://rustdesk.com/server)하거나 [직접 rendezvous/relay 서버를 작성할 수도 있습니다](https://github.com/rustdesk/rustdesk-server-demo).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -43,9 +43,9 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C
|
|||||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||||
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
||||||
|
|
||||||
- run `cargo run`
|
- 실행 `cargo run`
|
||||||
|
|
||||||
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
## [빌드](https://rustdesk.com/docs/en/dev/build/)
|
||||||
|
|
||||||
## Linux에서 빌드 순서
|
## Linux에서 빌드 순서
|
||||||
|
|
||||||
@@ -67,7 +67,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
|
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
|
```sh
|
||||||
git clone https://github.com/microsoft/vcpkg
|
git clone https://github.com/microsoft/vcpkg
|
||||||
@@ -79,7 +79,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
|||||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fix libvpx (For Fedora)
|
### libvpx 수정 (For Fedora용)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd vcpkg/buildtrees/libvpx/src
|
cd vcpkg/buildtrees/libvpx/src
|
||||||
@@ -92,7 +92,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
|||||||
cd
|
cd
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build
|
### 빌드
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
@@ -107,7 +107,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
|
|||||||
|
|
||||||
## Docker에 빌드하는 방법
|
## Docker에 빌드하는 방법
|
||||||
|
|
||||||
레포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
|
리포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/rustdesk/rustdesk
|
git clone https://github.com/rustdesk/rustdesk
|
||||||
@@ -115,13 +115,13 @@ cd rustdesk
|
|||||||
docker build -t "rustdesk-builder" .
|
docker build -t "rustdesk-builder" .
|
||||||
```
|
```
|
||||||
|
|
||||||
이후, 애플리케이션을 빌드할 필요가 있을 때마다, 이하의 커맨드를 실행합니다.
|
이후, 애플리케이션을 빌드할 필요가 있을 때마다, 아래의의 명령을 실행합니다.
|
||||||
|
|
||||||
```sh
|
```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
|
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||||
```
|
```
|
||||||
|
|
||||||
첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 커맨드에 다른 인수를 지정할 필요가 있다면, 커맨드 끝에 있는 `<OPTIONAL-ARGS>` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 커맨드 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 커맨드로 실행할 수 있습니다.
|
첫 빌드에서는 의존관계가 캐시될 때까지 시간이 걸릴 수 있습니다만, 이후의 빌드때는 빨라집니다. 더불어 빌드 명령에 다른 인수를 지정할 필요가 있다면, 명령 끝에 있는 `<OPTIONAL-ARGS>` 에 지정할 수 있습니다. 예를 들어 최적화된 출시 버전을 빌드하고 싶다면 이렇게 상기한 명령 뒤에 `--release` 를 붙여 실행합니다. 성공했다면 실행파일은 시스템 타겟 폴더에 담겨지고, 다음 명령으로 실행할 수 있습니다.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
target/debug/rustdesk
|
target/debug/rustdesk
|
||||||
@@ -133,9 +133,9 @@ target/debug/rustdesk
|
|||||||
target/release/rustdesk
|
target/release/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
커맨드를 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 서브커맨드는 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다.
|
명령을 RustDesk 리포지토리 루트에서 실행한다는 것을 확인해주세요. 그렇게 하지 않으면 애플리케이션이 필요한 리소스를 발견하지 못 할 가능성이 있습니다. 또한 `install`, `run` 같은 cargo 하위 명령은 호스트가 아니라 컨테이너 프로그램을 설치, 실행을 위함이므로 현재 이 방법은 지원하지 않다는 점을 유념해주시길 바랍니다.
|
||||||
|
|
||||||
## File Structure
|
## 파일 구조
|
||||||
|
|
||||||
- **[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/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처
|
||||||
@@ -143,12 +143,12 @@ target/release/rustdesk
|
|||||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오, 클립보드, 입력, 비디오 서비스 그리고 네트워크 연결
|
- **[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/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 접속 시작
|
||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다이렉트(TCP hole punching) 혹은 relayed 접속
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다이렉트 (TCP hole punching) 혹은 relayed 접속
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼 고유의 코드
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼 고유의 코드
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트
|
||||||
|
|
||||||
## Snapshot
|
## 스냅샷
|
||||||
|
|
||||||

|

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

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
|
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
|
||||||
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> •
|
<a href="#публічні-сервери">Сервери</a> •
|
||||||
<a href="#кроки-для-збірки">Збирання</a> •
|
<a href="#кроки-для-збірки">Збирання</a> •
|
||||||
<a href="#як-зібрати-за-допомогою-docker">Docker</a> •
|
<a href="#як-зібрати-за-допомогою-docker">Docker</a> •
|
||||||
<a href="#структура-файлів">Структура</a> •
|
<a href="#структура-файлів">Структура</a> •
|
||||||
<a href="#знімки">Знімки</a><br>
|
<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>
|
[<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>
|
<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>
|
</p>
|
||||||
|
|
||||||
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||||
|
|
||||||
[](https://ko-fi.com/I2I04VU09)
|
[](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).
|
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
||||||
|
|
||||||

|

|
||||||
@@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
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 \
|
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
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```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)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```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)
|
### Arch (Manjaro)
|
||||||
@@ -158,18 +156,19 @@ target/release/rustdesk
|
|||||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS.
|
- **[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/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
|
||||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень
|
- **[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/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове зʼєднання
|
||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
|
- **[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)**: JavaScript для Flutter веб клієнту
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
|
||||||
|
|
||||||
## Знімки
|
## Знімки екрана
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||

|
|
||||||
|
|||||||
@@ -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>
|
[<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>
|
</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)
|
[](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)
|
[**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) |
|
[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) |
|
[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之前插入两行:
|
在Dockerfile的RUN apt update之前插入两行:
|
||||||
|
|
||||||
RUN sed -i "s/deb.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 && \
|
||||||
RUN sed -i "s/security.debian.org/mirrors.163.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`后插入下面代码:
|
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/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/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
|
||||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
|
- **[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/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
|
||||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
|
- **[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/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)**: 平台服务相关代码
|
- **[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代码
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
|
||||||
|
|
||||||
## 截图
|
## 截图
|
||||||
|
|||||||
59
flatpak/com.rustdesk.RustDesk.metainfo.xml
Normal file
59
flatpak/com.rustdesk.RustDesk.metainfo.xml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.rustdesk.RustDesk</id>
|
||||||
|
<developer id="com.rustdesk">
|
||||||
|
<name>RustDesk</name>
|
||||||
|
</developer>
|
||||||
|
<launchable type="desktop-id">com.rustdesk.RustDesk.desktop</launchable>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>AGPL-3.0-only</project_license>
|
||||||
|
<name>RustDesk</name>
|
||||||
|
<summary>Secure remote desktop access</summary>
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li> Works on Windows, macOS, Linux, iOS, Android, Web. </li>
|
||||||
|
<li> Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs. </li>
|
||||||
|
<li> Own your data, easily set up self-hosting solution on your infrastructure. </li>
|
||||||
|
<li> P2P connection with end-to-end encryption based on NaCl. </li>
|
||||||
|
<li> No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand. </li>
|
||||||
|
<li> We like to keep things simple and will strive to make simpler where possible. </li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
For self-hosting setup instructions please go to our home page.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
<categories>
|
||||||
|
<category>Utility</category>
|
||||||
|
</categories>
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<caption>Remote desktop session</caption>
|
||||||
|
<image>https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png</image>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
<branding>
|
||||||
|
<color type="primary" scheme_preference="light">#d9eaf8</color>
|
||||||
|
<color type="primary" scheme_preference="dark">#0160ee</color>
|
||||||
|
</branding>
|
||||||
|
<url type="homepage">https://rustdesk.com</url>
|
||||||
|
<url type="bugtracker">https://github.com/rustdesk/rustdesk/issues</url>
|
||||||
|
<url type="faq">https://github.com/rustdesk/rustdesk/wiki/FAQ</url>
|
||||||
|
<url type="help">https://rustdesk.com/docs</url>
|
||||||
|
<url type="donation">https://ko-fi.com/rustdesk</url>
|
||||||
|
<url type="vcs-browser">https://github.com/rustdesk/rustdesk</url>
|
||||||
|
<url type="translate">https://github.com/rustdesk/rustdesk/tree/master/src/lang</url>
|
||||||
|
<url type="contribute">https://github.com/rustdesk/rustdesk/blob/master/docs/CONTRIBUTING.md</url>
|
||||||
|
<url type="contact">https://rustdesk.com/docs/en/technical-support</url>
|
||||||
|
<requires>
|
||||||
|
<display_length compare="ge">600</display_length>
|
||||||
|
<internet>always</internet>
|
||||||
|
</requires>
|
||||||
|
<supports>
|
||||||
|
<control>keyboard</control>
|
||||||
|
<control>pointing</control>
|
||||||
|
</supports>
|
||||||
|
<content_rating type="oars-1.1"/>
|
||||||
|
</component>
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
{
|
{
|
||||||
"id": "com.rustdesk.RustDesk",
|
"id": "com.rustdesk.RustDesk",
|
||||||
"runtime": "org.freedesktop.Platform",
|
"runtime": "org.freedesktop.Platform",
|
||||||
"runtime-version": "23.08",
|
"runtime-version": "24.08",
|
||||||
"sdk": "org.freedesktop.Sdk",
|
"sdk": "org.freedesktop.Sdk",
|
||||||
"command": "rustdesk",
|
"command": "rustdesk",
|
||||||
"icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
|
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
|
||||||
|
"rename-desktop-file": "rustdesk.desktop",
|
||||||
|
"rename-icon": "rustdesk",
|
||||||
"modules": [
|
"modules": [
|
||||||
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
|
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
|
||||||
"xdotool.json",
|
|
||||||
{
|
{
|
||||||
"name": "pam",
|
"name": "xdotool",
|
||||||
"buildsystem": "simple",
|
"no-autogen": true,
|
||||||
"build-commands": [
|
"make-install-args": ["PREFIX=${FLATPAK_DEST}"],
|
||||||
"./configure --disable-selinux --prefix=/app && make -j4 install"
|
"sources": [
|
||||||
],
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz",
|
||||||
|
"sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pam",
|
||||||
|
"buildsystem": "autotools",
|
||||||
|
"config-opts": ["--disable-selinux"],
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
@@ -26,32 +37,24 @@
|
|||||||
"name": "rustdesk",
|
"name": "rustdesk",
|
||||||
"buildsystem": "simple",
|
"buildsystem": "simple",
|
||||||
"build-commands": [
|
"build-commands": [
|
||||||
"bsdtar -zxvf rustdesk.deb",
|
"bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -",
|
||||||
"tar -xvf ./data.tar.xz",
|
"cp -r usr/* /app/",
|
||||||
"cp -r ./usr/* /app/",
|
"mkdir -p /app/bin && ln -s /app/share/rustdesk/rustdesk /app/bin/rustdesk"
|
||||||
"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",
|
|
||||||
"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"
|
|
||||||
],
|
],
|
||||||
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
|
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "./rustdesk.deb"
|
"path": "rustdesk.deb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "../res/scalable.svg"
|
"path": "com.rustdesk.RustDesk.metainfo.xml"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"finish-args": [
|
"finish-args": [
|
||||||
"--share=ipc",
|
"--share=ipc",
|
||||||
"--socket=x11",
|
|
||||||
"--socket=fallback-x11",
|
"--socket=fallback-x11",
|
||||||
"--socket=wayland",
|
"--socket=wayland",
|
||||||
"--share=network",
|
"--share=network",
|
||||||
@@ -60,4 +63,4 @@
|
|||||||
"--socket=pulseaudio",
|
"--socket=pulseaudio",
|
||||||
"--talk-name=org.freedesktop.Flatpak"
|
"--talk-name=org.freedesktop.Flatpak"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "xdotool",
|
|
||||||
"buildsystem": "simple",
|
|
||||||
"build-commands": [
|
|
||||||
"make -j4 && PREFIX=./build make install",
|
|
||||||
"cp -r ./build/* /app/"
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "archive",
|
|
||||||
"url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz",
|
|
||||||
"sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import com.google.protobuf.gradle.*
|
import com.google.protobuf.gradle.*
|
||||||
plugins {
|
plugins {
|
||||||
id "com.google.protobuf" version "0.9.4"
|
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()
|
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')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = '1'
|
flutterVersionCode = '1'
|
||||||
@@ -32,10 +30,6 @@ if (flutterVersionName == null) {
|
|||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
|
||||||
}
|
}
|
||||||
@@ -57,7 +51,7 @@ protobuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdkVersion 34
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
|
||||||
@@ -105,7 +99,6 @@ flutter {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.media:media:1.6.0"
|
implementation "androidx.media:media:1.6.0"
|
||||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
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'
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,13 @@
|
|||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<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
|
<application
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="RustDesk"
|
android:label="RustDesk"
|
||||||
|
|||||||
@@ -304,7 +304,13 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
|||||||
val popupMenu = PopupMenu(this, floatingView)
|
val popupMenu = PopupMenu(this, floatingView)
|
||||||
val idShowRustDesk = 0
|
val idShowRustDesk = 0
|
||||||
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
|
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
|
||||||
val idStopService = 1
|
// For host side, clipboard sync
|
||||||
|
val idSyncClipboard = 1
|
||||||
|
val isServiceSyncEnabled = (MainActivity.rdClipboardManager?.isCaptureStarted ?: false) && FFI.isServiceClipboardEnabled()
|
||||||
|
if (isServiceSyncEnabled) {
|
||||||
|
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
|
||||||
|
}
|
||||||
|
val idStopService = 2
|
||||||
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||||
when (menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
@@ -312,6 +318,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
|||||||
openMainActivity()
|
openMainActivity()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
idSyncClipboard -> {
|
||||||
|
syncClipboard()
|
||||||
|
true
|
||||||
|
}
|
||||||
idStopService -> {
|
idStopService -> {
|
||||||
stopMainService()
|
stopMainService()
|
||||||
true
|
true
|
||||||
@@ -340,6 +350,10 @@ class FloatingWindowService : Service(), View.OnTouchListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun syncClipboard() {
|
||||||
|
MainActivity.rdClipboardManager?.syncClipboard(false)
|
||||||
|
}
|
||||||
|
|
||||||
private fun stopMainService() {
|
private fun stopMainService() {
|
||||||
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
|
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,20 +280,20 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
var textToCommit: String? = null
|
var textToCommit: String? = null
|
||||||
|
|
||||||
if (keyboardMode == KeyboardMode.Legacy) {
|
// [down] indicates the key's state(down or up).
|
||||||
if (keyEvent.hasChr() && keyEvent.getDown()) {
|
// [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()
|
val chr = keyEvent.getChr()
|
||||||
if (chr != null) {
|
if (chr != null) {
|
||||||
textToCommit = String(Character.toChars(chr))
|
textToCommit = String(Character.toChars(chr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (keyboardMode == KeyboardMode.Translate) {
|
} else if (keyboardMode == KeyboardMode.Translate) {
|
||||||
if (keyEvent.hasSeq() && keyEvent.getDown()) {
|
} else {
|
||||||
val seq = keyEvent.getSeq()
|
|
||||||
if (seq != null) {
|
|
||||||
textToCommit = seq
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
||||||
@@ -320,6 +320,10 @@ class InputService : AccessibilityService() {
|
|||||||
} else {
|
} else {
|
||||||
ke?.let { event ->
|
ke?.let { event ->
|
||||||
inputConnection.sendKeyEvent(event)
|
inputConnection.sendKeyEvent(event)
|
||||||
|
if (keyEvent.getPress()) {
|
||||||
|
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
|
||||||
|
inputConnection.sendKeyEvent(actionUpEvent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,6 +337,10 @@ class InputService : AccessibilityService() {
|
|||||||
for (item in possibleNodes) {
|
for (item in possibleNodes) {
|
||||||
val success = trySendKeyEvent(event, item, textToCommit)
|
val success = trySendKeyEvent(event, item, textToCommit)
|
||||||
if (success) {
|
if (success) {
|
||||||
|
if (keyEvent.getPress()) {
|
||||||
|
val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode)
|
||||||
|
trySendKeyEvent(actionUpEvent, item, textToCommit)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,12 @@ object KeyEventConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var action = 0
|
var action = 0
|
||||||
if (keyEventProto.getDown()) {
|
if (keyEventProto.getDown() || keyEventProto.getPress()) {
|
||||||
action = KeyEvent.ACTION_DOWN
|
action = KeyEvent.ACTION_DOWN
|
||||||
} else {
|
} else {
|
||||||
action = KeyEvent.ACTION_UP
|
action = KeyEvent.ACTION_UP
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: The last parameter is the repeat count, not modifiers ?
|
|
||||||
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
|
|
||||||
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -36,6 +38,9 @@ import kotlin.concurrent.thread
|
|||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
var flutterMethodChannel: MethodChannel? = null
|
var flutterMethodChannel: MethodChannel? = null
|
||||||
|
private var _rdClipboardManager: RdClipboardManager? = null
|
||||||
|
val rdClipboardManager: RdClipboardManager?
|
||||||
|
get() = _rdClipboardManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private val channelTag = "mChannel"
|
private val channelTag = "mChannel"
|
||||||
@@ -85,6 +90,14 @@ 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() {
|
override fun onDestroy() {
|
||||||
Log.e(logTag, "onDestroy")
|
Log.e(logTag, "onDestroy")
|
||||||
mainService?.let {
|
mainService?.let {
|
||||||
@@ -207,6 +220,10 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(true)
|
result.success(true)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
"try_sync_clipboard" -> {
|
||||||
|
rdClipboardManager?.syncClipboard(true)
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
GET_START_ON_BOOT_OPT -> {
|
GET_START_ON_BOOT_OPT -> {
|
||||||
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
|
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
|
||||||
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
|
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
|
||||||
|
|||||||
@@ -302,6 +302,8 @@ class MainService : Service() {
|
|||||||
stopCapture()
|
stopCapture()
|
||||||
FFI.refreshScreen()
|
FFI.refreshScreen()
|
||||||
startCapture()
|
startCapture()
|
||||||
|
} else {
|
||||||
|
FFI.refreshScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,6 +433,7 @@ class MainService : Service() {
|
|||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
_isStart = true
|
_isStart = true
|
||||||
FFI.setFrameRawEnable("video",true)
|
FFI.setFrameRawEnable("video",true)
|
||||||
|
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +442,7 @@ class MainService : Service() {
|
|||||||
Log.d(logTag, "Stop Capture")
|
Log.d(logTag, "Stop Capture")
|
||||||
FFI.setFrameRawEnable("video",false)
|
FFI.setFrameRawEnable("video",false)
|
||||||
_isStart = false
|
_isStart = false
|
||||||
|
MainActivity.rdClipboardManager?.setCaptureStarted(_isStart)
|
||||||
// release video
|
// release video
|
||||||
if (reuseVirtualDisplay) {
|
if (reuseVirtualDisplay) {
|
||||||
// The virtual display video projection can be paused by calling `setSurface(null)`.
|
// The virtual display video projection can be paused by calling `setSurface(null)`.
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
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 _isCaptureStarted = false;
|
||||||
|
|
||||||
|
val isCaptureStarted: Boolean
|
||||||
|
get() = _isCaptureStarted
|
||||||
|
|
||||||
|
fun checkPrimaryClip(isClient: 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)
|
||||||
|
// Ignore the `isClipboardDataEqual()` check if it's a host operation.
|
||||||
|
// Because it's an action manually triggered by the user.
|
||||||
|
if (isClient) {
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCaptureStarted(started: Boolean) {
|
||||||
|
_isCaptureStarted = started
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
fun rustEnableClientClipboard(enable: Boolean) {
|
||||||
|
Log.d(logTag, "rustEnableClientClipboard: enable: $enable")
|
||||||
|
isClientEnabled = enable
|
||||||
|
lastUpdatedClipData = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncClipboard(isClient: Boolean) {
|
||||||
|
Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled")
|
||||||
|
if (isClient && !isClientEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
checkPrimaryClip(isClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,15 @@ package ffi
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
import com.carriez.flutter_hbb.RdClipboardManager
|
||||||
|
|
||||||
object FFI {
|
object FFI {
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("rustdesk")
|
System.loadLibrary("rustdesk")
|
||||||
}
|
}
|
||||||
|
|
||||||
external fun init(ctx: Context)
|
external fun init(ctx: Context)
|
||||||
|
external fun setClipboardManager(clipboardManager: RdClipboardManager)
|
||||||
external fun startServer(app_dir: String, custom_client_config: String)
|
external fun startServer(app_dir: String, custom_client_config: String)
|
||||||
external fun startService()
|
external fun startService()
|
||||||
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||||
@@ -20,4 +23,6 @@ object FFI {
|
|||||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||||
external fun setCodecInfo(info: String)
|
external fun setCodecInfo(info: String)
|
||||||
external fun getLocalOption(key: String): String
|
external fun getLocalOption(key: String): String
|
||||||
}
|
external fun onClipboardUpdate(clips: ByteBuffer)
|
||||||
|
external fun isServiceClipboardEnabled(): Boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -24,6 +9,8 @@ allprojects {
|
|||||||
rootProject.buildDir = '../build'
|
rootProject.buildDir = '../build'
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
def properties = new Properties()
|
|
||||||
|
|
||||||
assert localPropertiesFile.exists()
|
repositories {
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
plugins {
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
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 |
@@ -68,6 +68,7 @@ function build {
|
|||||||
pushd "$SCRIPTDIR/.."
|
pushd "$SCRIPTDIR/.."
|
||||||
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
|
||||||
popd
|
popd
|
||||||
|
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
|
||||||
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
|
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
|
||||||
|
|
||||||
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
|
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -x
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Script to build F-Droid release of RustDesk
|
# Script to build F-Droid release of RustDesk
|
||||||
#
|
#
|
||||||
@@ -23,6 +21,43 @@ set -x
|
|||||||
# + build: perform actual build of APK file
|
# + 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
|
# Parse command-line arguments
|
||||||
|
|
||||||
VERNAME="${1}"
|
VERNAME="${1}"
|
||||||
@@ -43,15 +78,13 @@ arm64-v8a)
|
|||||||
FLUTTER_TARGET=android-arm64
|
FLUTTER_TARGET=android-arm64
|
||||||
NDK_TARGET=aarch64-linux-android
|
NDK_TARGET=aarch64-linux-android
|
||||||
RUST_TARGET=aarch64-linux-android
|
RUST_TARGET=aarch64-linux-android
|
||||||
# RUSTDESK_FEATURES='flutter,hwcodec'
|
RUSTDESK_FEATURES='flutter,hwcodec'
|
||||||
RUSTDESK_FEATURES='flutter'
|
|
||||||
;;
|
;;
|
||||||
armeabi-v7a)
|
armeabi-v7a)
|
||||||
FLUTTER_TARGET=android-arm
|
FLUTTER_TARGET=android-arm
|
||||||
NDK_TARGET=arm-linux-androideabi
|
NDK_TARGET=arm-linux-androideabi
|
||||||
RUST_TARGET=armv7-linux-androideabi
|
RUST_TARGET=armv7-linux-androideabi
|
||||||
# RUSTDESK_FEATURES='flutter,hwcodec'
|
RUSTDESK_FEATURES='flutter,hwcodec'
|
||||||
RUSTDESK_FEATURES='flutter'
|
|
||||||
;;
|
;;
|
||||||
x86_64)
|
x86_64)
|
||||||
FLUTTER_TARGET=android-x64
|
FLUTTER_TARGET=android-x64
|
||||||
@@ -103,18 +136,27 @@ prebuild)
|
|||||||
.env.CARGO_NDK_VERSION \
|
.env.CARGO_NDK_VERSION \
|
||||||
.github/workflows/flutter-build.yml)"
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
# Flutter used to compile main Rustdesk library
|
||||||
|
|
||||||
FLUTTER_VERSION="$(yq -r \
|
FLUTTER_VERSION="$(yq -r \
|
||||||
.env.ANDROID_FLUTTER_VERSION \
|
.env.ANDROID_FLUTTER_VERSION \
|
||||||
.github/workflows/flutter-build.yml)"
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
if [ -z "${FLUTTER_VERSION}" ]; then
|
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||||
FLUTTER_VERSION="$(yq -r \
|
FLUTTER_VERSION="$(yq -r \
|
||||||
.env.FLUTTER_VERSION \
|
.env.FLUTTER_VERSION \
|
||||||
.github/workflows/flutter-build.yml)"
|
.github/workflows/flutter-build.yml)"
|
||||||
fi
|
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 \
|
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
|
||||||
.env.FLUTTER_RUST_BRIDGE_VERSION \
|
.env.FLUTTER_RUST_BRIDGE_VERSION \
|
||||||
.github/workflows/flutter-build.yml)"
|
.github/workflows/bridge.yml)"
|
||||||
|
|
||||||
NDK_VERSION="$(yq -r \
|
NDK_VERSION="$(yq -r \
|
||||||
.env.NDK_VERSION \
|
.env.NDK_VERSION \
|
||||||
@@ -129,6 +171,7 @@ prebuild)
|
|||||||
.github/workflows/flutter-build.yml)"
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
|
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
|
||||||
|
[ -z "${FLUTTER_BRIDGE_VERSION}" ] ||
|
||||||
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
|
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
|
||||||
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
|
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
|
||||||
[ -z "${VCPKG_COMMIT_ID}" ]; then
|
[ -z "${VCPKG_COMMIT_ID}" ]; then
|
||||||
@@ -165,24 +208,6 @@ prebuild)
|
|||||||
sdkmanager --install "ndk;${NDK_VERSION}"
|
sdkmanager --install "ndk;${NDK_VERSION}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Flutter
|
|
||||||
|
|
||||||
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
|
|
||||||
pushd "${HOME}"
|
|
||||||
|
|
||||||
git clone https://github.com/flutter/flutter
|
|
||||||
|
|
||||||
pushd flutter
|
|
||||||
|
|
||||||
git reset --hard "${FLUTTER_VERSION}"
|
|
||||||
|
|
||||||
flutter config --no-analytics
|
|
||||||
|
|
||||||
popd # flutter
|
|
||||||
|
|
||||||
popd # ${HOME}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install Rust
|
# Install Rust
|
||||||
|
|
||||||
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
|
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
|
||||||
@@ -207,14 +232,18 @@ prebuild)
|
|||||||
|
|
||||||
cargo install \
|
cargo install \
|
||||||
cargo-ndk \
|
cargo-ndk \
|
||||||
--version "${CARGO_NDK_VERSION}"
|
--version "${CARGO_NDK_VERSION}" \
|
||||||
|
--locked
|
||||||
|
|
||||||
# Install rust bridge generator
|
# Install rust bridge generator
|
||||||
|
|
||||||
cargo install cargo-expand
|
cargo install \
|
||||||
|
cargo-expand \
|
||||||
|
--locked
|
||||||
cargo install flutter_rust_bridge_codegen \
|
cargo install flutter_rust_bridge_codegen \
|
||||||
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
|
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
|
||||||
--features "uuid"
|
--features "uuid" \
|
||||||
|
--locked
|
||||||
|
|
||||||
# Populate native vcpkg dependencies
|
# Populate native vcpkg dependencies
|
||||||
|
|
||||||
@@ -277,12 +306,66 @@ prebuild)
|
|||||||
|
|
||||||
git apply res/fdroid/patches/*.patch
|
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 \
|
sed \
|
||||||
-i \
|
-i \
|
||||||
-e '/gms/d' \
|
-e '/gms/d' \
|
||||||
flutter/android/build.gradle \
|
flutter/android/build.gradle \
|
||||||
flutter/android/app/build.gradle
|
flutter/android/app/build.gradle
|
||||||
|
|
||||||
|
# `firebase_analytics` is not in these files now, but we still keep the following lines.
|
||||||
|
|
||||||
sed \
|
sed \
|
||||||
-i \
|
-i \
|
||||||
-e '/firebase_analytics/d' \
|
-e '/firebase_analytics/d' \
|
||||||
@@ -298,33 +381,6 @@ prebuild)
|
|||||||
-e '/firebase/Id' \
|
-e '/firebase/Id' \
|
||||||
flutter/lib/main.dart
|
flutter/lib/main.dart
|
||||||
|
|
||||||
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
|
|
||||||
# Fix for android 3.13.9
|
|
||||||
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
|
|
||||||
|
|
||||||
sed \
|
|
||||||
-i \
|
|
||||||
-e 's/uni_links_desktop/#uni_links_desktop/g' \
|
|
||||||
flutter/pubspec.yaml
|
|
||||||
|
|
||||||
set --
|
|
||||||
|
|
||||||
while read -r _1; do
|
|
||||||
set -- "$@" "${_1}"
|
|
||||||
done 0<<.a
|
|
||||||
$(find flutter/lib/ -type f -name "*dart*")
|
|
||||||
.a
|
|
||||||
|
|
||||||
sed \
|
|
||||||
-i \
|
|
||||||
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
set --
|
|
||||||
fi
|
|
||||||
|
|
||||||
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
|
|
||||||
|
|
||||||
;;
|
;;
|
||||||
build)
|
build)
|
||||||
# build: perform actual build of APK file
|
# build: perform actual build of APK file
|
||||||
@@ -336,9 +392,12 @@ build)
|
|||||||
# '.github/workflows/flutter-build.yml'
|
# '.github/workflows/flutter-build.yml'
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Flutter used to compile main Rustdesk library
|
||||||
|
|
||||||
FLUTTER_VERSION="$(yq -r \
|
FLUTTER_VERSION="$(yq -r \
|
||||||
.env.ANDROID_FLUTTER_VERSION \
|
.env.ANDROID_FLUTTER_VERSION \
|
||||||
.github/workflows/flutter-build.yml)"
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
if [ -z "${FLUTTER_VERSION}" ]; then
|
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||||
FLUTTER_VERSION="$(yq -r \
|
FLUTTER_VERSION="$(yq -r \
|
||||||
.env.FLUTTER_VERSION \
|
.env.FLUTTER_VERSION \
|
||||||
@@ -374,16 +433,11 @@ build)
|
|||||||
|
|
||||||
pushd flutter
|
pushd flutter
|
||||||
|
|
||||||
|
flutter clean
|
||||||
flutter packages pub get
|
flutter packages pub get
|
||||||
|
|
||||||
popd # flutter
|
popd # flutter
|
||||||
|
|
||||||
# Generate FFI bindings
|
|
||||||
|
|
||||||
flutter_rust_bridge_codegen \
|
|
||||||
--rust-input ./src/flutter_ffi.rs \
|
|
||||||
--dart-output ./flutter/lib/generated_bridge.dart
|
|
||||||
|
|
||||||
# Build host android deps
|
# Build host android deps
|
||||||
|
|
||||||
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
|
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
|
||||||
|
|||||||
@@ -2,4 +2,6 @@
|
|||||||
# https://docs.flutter.dev/deployment/ios
|
# https://docs.flutter.dev/deployment/ios
|
||||||
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
|
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
|
||||||
# no obfuscate, because no easy to check errors
|
# no obfuscate, because no easy to check errors
|
||||||
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
|
git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
flutter build ipa --release
|
flutter build ipa --release
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
cd build/web/
|
|
||||||
python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);'
|
|
||||||
mv jds/dist/index.js ./
|
|
||||||
mv jds/dist/vendor.js ./
|
|
||||||
/bin/rm -rf js
|
|
||||||
python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)'
|
|
||||||
python3 -c 'import hashlib;x=hashlib.sha1(open("./index.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/index.js", "index.js?v="+x);open("index.html","wt").write(y)'
|
|
||||||
python3 -c 'import hashlib;x=hashlib.sha1(open("./vendor.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/vendor.js", "vendor.js?v="+x);open("index.html","wt").write(y)'
|
|
||||||
tar czf x *
|
|
||||||
scp x sg:/tmp/
|
|
||||||
ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R"
|
|
||||||
/bin/rm x
|
|
||||||
cd -
|
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>12.0</string>
|
<string>13.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</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.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
platform :ios, '12.0'
|
platform :ios, '13.0'
|
||||||
|
|
||||||
project 'Runner', {
|
project 'Runner', {
|
||||||
'Debug' => :debug,
|
'Debug' => :debug,
|
||||||
|
|||||||
@@ -133,10 +133,10 @@ SPEC CHECKSUMS:
|
|||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||||
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
|
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
|
||||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||||
|
|
||||||
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
|
PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.15.2
|
||||||
|
|||||||
@@ -347,7 +347,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -491,7 +491,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -541,7 +541,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
@UIApplicationMain
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
|
git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ import 'common/widgets/overlay.dart';
|
|||||||
import 'mobile/pages/file_manager_page.dart';
|
import 'mobile/pages/file_manager_page.dart';
|
||||||
import 'mobile/pages/remote_page.dart';
|
import 'mobile/pages/remote_page.dart';
|
||||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||||
|
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
|
||||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||||
import 'models/input_model.dart';
|
|
||||||
import 'models/model.dart';
|
import 'models/model.dart';
|
||||||
import 'models/platform_model.dart';
|
import 'models/platform_model.dart';
|
||||||
|
|
||||||
@@ -51,6 +51,9 @@ final isLinux = isLinux_;
|
|||||||
final isDesktop = isDesktop_;
|
final isDesktop = isDesktop_;
|
||||||
final isWeb = isWeb_;
|
final isWeb = isWeb_;
|
||||||
final isWebDesktop = isWebDesktop_;
|
final isWebDesktop = isWebDesktop_;
|
||||||
|
final isWebOnWindows = isWebOnWindows_;
|
||||||
|
final isWebOnLinux = isWebOnLinux_;
|
||||||
|
final isWebOnMacOs = isWebOnMacOS_;
|
||||||
var isMobile = isAndroid || isIOS;
|
var isMobile = isAndroid || isIOS;
|
||||||
var version = '';
|
var version = '';
|
||||||
int androidVersion = 0;
|
int androidVersion = 0;
|
||||||
@@ -61,6 +64,9 @@ int androidVersion = 0;
|
|||||||
// So we need to use this flag to enable/disable resizable.
|
// So we need to use this flag to enable/disable resizable.
|
||||||
bool _linuxWindowResizable = true;
|
bool _linuxWindowResizable = true;
|
||||||
|
|
||||||
|
// Only used on Windows(window manager).
|
||||||
|
bool _ignoreDevicePixelRatio = true;
|
||||||
|
|
||||||
/// only available for Windows target
|
/// only available for Windows target
|
||||||
int windowsBuildNumber = 0;
|
int windowsBuildNumber = 0;
|
||||||
DesktopType? desktopType;
|
DesktopType? desktopType;
|
||||||
@@ -348,6 +354,9 @@ class MyTheme {
|
|||||||
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
||||||
scaffoldBackgroundColor: Colors.white,
|
scaffoldBackgroundColor: Colors.white,
|
||||||
dialogBackgroundColor: Colors.white,
|
dialogBackgroundColor: Colors.white,
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
),
|
||||||
dialogTheme: DialogTheme(
|
dialogTheme: DialogTheme(
|
||||||
elevation: 15,
|
elevation: 15,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -443,6 +452,9 @@ class MyTheme {
|
|||||||
hoverColor: Color.fromARGB(255, 45, 46, 53),
|
hoverColor: Color.fromARGB(255, 45, 46, 53),
|
||||||
scaffoldBackgroundColor: Color(0xFF18191E),
|
scaffoldBackgroundColor: Color(0xFF18191E),
|
||||||
dialogBackgroundColor: Color(0xFF18191E),
|
dialogBackgroundColor: Color(0xFF18191E),
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
),
|
||||||
dialogTheme: DialogTheme(
|
dialogTheme: DialogTheme(
|
||||||
elevation: 15,
|
elevation: 15,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -546,9 +558,9 @@ class MyTheme {
|
|||||||
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
|
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void changeDarkMode(ThemeMode mode) async {
|
static Future<void> changeDarkMode(ThemeMode mode) async {
|
||||||
Get.changeThemeMode(mode);
|
Get.changeThemeMode(mode);
|
||||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
|
||||||
if (mode == ThemeMode.system) {
|
if (mode == ThemeMode.system) {
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
||||||
@@ -556,7 +568,7 @@ class MyTheme {
|
|||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||||
}
|
}
|
||||||
await bind.mainChangeTheme(dark: mode.toShortString());
|
if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
|
||||||
// Synchronize the window theme of the system.
|
// Synchronize the window theme of the system.
|
||||||
updateSystemWindowTheme();
|
updateSystemWindowTheme();
|
||||||
}
|
}
|
||||||
@@ -630,10 +642,30 @@ List<Locale> supportedLocales = const [
|
|||||||
Locale('da'),
|
Locale('da'),
|
||||||
Locale('eo'),
|
Locale('eo'),
|
||||||
Locale('tr'),
|
Locale('tr'),
|
||||||
Locale('vi'),
|
|
||||||
Locale('pl'),
|
|
||||||
Locale('kz'),
|
Locale('kz'),
|
||||||
Locale('es'),
|
Locale('es'),
|
||||||
|
Locale('nl'),
|
||||||
|
Locale('nb'),
|
||||||
|
Locale('et'),
|
||||||
|
Locale('eu'),
|
||||||
|
Locale('bg'),
|
||||||
|
Locale('be'),
|
||||||
|
Locale('vn'),
|
||||||
|
Locale('uk'),
|
||||||
|
Locale('fa'),
|
||||||
|
Locale('ca'),
|
||||||
|
Locale('el'),
|
||||||
|
Locale('sv'),
|
||||||
|
Locale('sq'),
|
||||||
|
Locale('sr'),
|
||||||
|
Locale('th'),
|
||||||
|
Locale('sl'),
|
||||||
|
Locale('ro'),
|
||||||
|
Locale('lt'),
|
||||||
|
Locale('lv'),
|
||||||
|
Locale('ar'),
|
||||||
|
Locale('he'),
|
||||||
|
Locale('hr'),
|
||||||
];
|
];
|
||||||
|
|
||||||
String formatDurationToTime(Duration duration) {
|
String formatDurationToTime(Duration duration) {
|
||||||
@@ -647,11 +679,17 @@ String formatDurationToTime(Duration duration) {
|
|||||||
|
|
||||||
closeConnection({String? id}) {
|
closeConnection({String? id}) {
|
||||||
if (isAndroid || isIOS) {
|
if (isAndroid || isIOS) {
|
||||||
gFFI.chatModel.hideChatOverlay();
|
() async {
|
||||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
|
overlays: SystemUiOverlay.values);
|
||||||
|
gFFI.chatModel.hideChatOverlay();
|
||||||
|
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||||
|
stateGlobal.isInMainPage = true;
|
||||||
|
}();
|
||||||
} else {
|
} else {
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||||
|
stateGlobal.isInMainPage = true;
|
||||||
} else {
|
} else {
|
||||||
final controller = Get.find<DesktopTabController>();
|
final controller = Get.find<DesktopTabController>();
|
||||||
controller.closeBy(id);
|
controller.closeBy(id);
|
||||||
@@ -1054,6 +1092,49 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget createDialogContent(String text) {
|
||||||
|
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
|
||||||
|
final List<TextSpan> spans = [];
|
||||||
|
int start = 0;
|
||||||
|
bool hasLink = false;
|
||||||
|
|
||||||
|
linkRegExp.allMatches(text).forEach((match) {
|
||||||
|
hasLink = true;
|
||||||
|
if (match.start > start) {
|
||||||
|
spans.add(TextSpan(text: text.substring(start, match.start)));
|
||||||
|
}
|
||||||
|
spans.add(TextSpan(
|
||||||
|
text: match.group(0) ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
String linkText = match.group(0) ?? '';
|
||||||
|
linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
|
||||||
|
launchUrl(Uri.parse(linkText));
|
||||||
|
},
|
||||||
|
));
|
||||||
|
start = match.end;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (start < text.length) {
|
||||||
|
spans.add(TextSpan(text: text.substring(start)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasLink) {
|
||||||
|
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectableText.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: TextStyle(color: Colors.black, fontSize: 15),
|
||||||
|
children: spans,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void msgBox(SessionID sessionId, String type, String title, String text,
|
void msgBox(SessionID sessionId, String type, String title, String text,
|
||||||
String link, OverlayDialogManager dialogManager,
|
String link, OverlayDialogManager dialogManager,
|
||||||
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
|
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
|
||||||
@@ -1096,33 +1177,21 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (reconnect != null && title == "Connection Error") {
|
if (reconnect != null &&
|
||||||
|
title == "Connection Error" &&
|
||||||
|
reconnectTimeout != null) {
|
||||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||||
final enabled = true.obs;
|
final enabled = true.obs;
|
||||||
final button = reconnectTimeout != null
|
final button = Obx(() => _ReconnectCountDownButton(
|
||||||
? Obx(() => _ReconnectCountDownButton(
|
second: reconnectTimeout,
|
||||||
second: reconnectTimeout,
|
onPressed: enabled.isTrue
|
||||||
onPressed: enabled.isTrue
|
? () {
|
||||||
? () {
|
// Disable the button
|
||||||
// Disable the button
|
enabled.value = false;
|
||||||
enabled.value = false;
|
reconnect(dialogManager, sessionId, false);
|
||||||
reconnect(dialogManager, sessionId, false);
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
));
|
||||||
))
|
|
||||||
: Obx(
|
|
||||||
() => dialogButton(
|
|
||||||
'Reconnect',
|
|
||||||
isOutline: true,
|
|
||||||
onPressed: enabled.isTrue
|
|
||||||
? () {
|
|
||||||
// Disable the button
|
|
||||||
enabled.value = false;
|
|
||||||
reconnect(dialogManager, sessionId, false);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
buttons.insert(0, button);
|
buttons.insert(0, button);
|
||||||
}
|
}
|
||||||
if (link.isNotEmpty) {
|
if (link.isNotEmpty) {
|
||||||
@@ -1211,7 +1280,7 @@ Widget msgboxContent(String type, String title, String text) {
|
|||||||
translate(title),
|
translate(title),
|
||||||
style: TextStyle(fontSize: 21),
|
style: TextStyle(fontSize: 21),
|
||||||
).marginOnly(bottom: 10),
|
).marginOnly(bottom: 10),
|
||||||
Text(translateText(text), style: const TextStyle(fontSize: 15)),
|
createDialogContent(translateText(text)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1406,14 +1475,10 @@ class AndroidPermissionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move this to mobile/widgets.
|
|
||||||
// Used only for mobile, pages remote, settings, dialog
|
|
||||||
// TODO remove argument contentPadding, it’s not used, getToggle() has not
|
|
||||||
RadioListTile<T> getRadio<T>(
|
RadioListTile<T> getRadio<T>(
|
||||||
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
|
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
|
||||||
{EdgeInsetsGeometry? contentPadding, bool? dense}) {
|
{bool? dense}) {
|
||||||
return RadioListTile<T>(
|
return RadioListTile<T>(
|
||||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
controlAffinity: ListTileControlAffinity.trailing,
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -1549,12 +1614,6 @@ Widget getPlatformImage(String platform, {double size = 50}) {
|
|||||||
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
||||||
}
|
}
|
||||||
|
|
||||||
class OffsetDevicePixelRatio {
|
|
||||||
Offset offset;
|
|
||||||
final double devicePixelRatio;
|
|
||||||
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
class LastWindowPosition {
|
class LastWindowPosition {
|
||||||
double? width;
|
double? width;
|
||||||
double? height;
|
double? height;
|
||||||
@@ -1637,8 +1696,10 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
|||||||
if (isFullscreen || isMaximized) {
|
if (isFullscreen || isMaximized) {
|
||||||
setPreFrame();
|
setPreFrame();
|
||||||
} else {
|
} else {
|
||||||
position = await windowManager.getPosition();
|
position = await windowManager.getPosition(
|
||||||
sz = await windowManager.getSize();
|
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||||
|
sz = await windowManager.getSize(
|
||||||
|
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -1756,7 +1817,7 @@ bool isPointInRect(Offset point, Rect rect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// return null means center
|
/// return null means center
|
||||||
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
Future<Offset?> _adjustRestoreMainWindowOffset(
|
||||||
double? left,
|
double? left,
|
||||||
double? top,
|
double? top,
|
||||||
double? width,
|
double? width,
|
||||||
@@ -1770,13 +1831,9 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
|||||||
double? frameTop;
|
double? frameTop;
|
||||||
double? frameRight;
|
double? frameRight;
|
||||||
double? frameBottom;
|
double? frameBottom;
|
||||||
double devicePixelRatio = 1.0;
|
|
||||||
|
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
for (final screen in await window_size.getScreenList()) {
|
for (final screen in await window_size.getScreenList()) {
|
||||||
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
|
|
||||||
devicePixelRatio = screen.scaleFactor;
|
|
||||||
}
|
|
||||||
frameLeft = frameLeft == null
|
frameLeft = frameLeft == null
|
||||||
? screen.visibleFrame.left
|
? screen.visibleFrame.left
|
||||||
: min(screen.visibleFrame.left, frameLeft);
|
: min(screen.visibleFrame.left, frameLeft);
|
||||||
@@ -1810,7 +1867,7 @@ Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
|||||||
top < frameTop!) {
|
top < frameTop!) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
|
return Offset(left, top);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1870,47 +1927,23 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
}
|
}
|
||||||
|
|
||||||
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
||||||
final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
|
final offsetLeftTop = await _adjustRestoreMainWindowOffset(
|
||||||
lpos.offsetWidth,
|
lpos.offsetWidth,
|
||||||
lpos.offsetHeight,
|
lpos.offsetHeight,
|
||||||
size.width,
|
size.width,
|
||||||
size.height,
|
size.height,
|
||||||
);
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
|
"restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case WindowType.Main:
|
case WindowType.Main:
|
||||||
// https://github.com/rustdesk/rustdesk/issues/8038
|
|
||||||
// `setBounds()` in `window_manager` will use the current devicePixelRatio.
|
|
||||||
// So we need to adjust the offset by the scale factor.
|
|
||||||
// https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
|
|
||||||
if (isWindows) {
|
|
||||||
double? curDevicePixelRatio;
|
|
||||||
Offset curPos = await windowManager.getPosition();
|
|
||||||
for (final screen in await window_size.getScreenList()) {
|
|
||||||
if (isPointInRect(curPos, screen.visibleFrame)) {
|
|
||||||
curDevicePixelRatio = screen.scaleFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (curDevicePixelRatio != null &&
|
|
||||||
curDevicePixelRatio != 0 &&
|
|
||||||
offsetDevicePixelRatio != null) {
|
|
||||||
if (offsetDevicePixelRatio.devicePixelRatio != 0) {
|
|
||||||
final scale =
|
|
||||||
offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
|
|
||||||
offsetDevicePixelRatio.offset =
|
|
||||||
offsetDevicePixelRatio.offset.scale(scale, scale);
|
|
||||||
debugPrint(
|
|
||||||
"restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
restorePos() async {
|
restorePos() async {
|
||||||
if (offsetDevicePixelRatio == null) {
|
if (offsetLeftTop == null) {
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
} else {
|
} else {
|
||||||
await windowManager.setPosition(offsetDevicePixelRatio.offset);
|
await windowManager.setPosition(offsetLeftTop,
|
||||||
|
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lpos.isMaximized == true) {
|
if (lpos.isMaximized == true) {
|
||||||
@@ -1919,20 +1952,39 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
await windowManager.maximize();
|
await windowManager.maximize();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) {
|
final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly();
|
||||||
await windowManager.setSize(size);
|
if (isWindows) {
|
||||||
|
if (storeSize) {
|
||||||
|
// We need to set the window size first to avoid the incorrect size in some special cases.
|
||||||
|
// E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI.
|
||||||
|
// The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor.
|
||||||
|
// After restoring, the size will be incorrect.
|
||||||
|
// See known issue in https://github.com/rustdesk/rustdesk/pull/9840
|
||||||
|
await windowManager.setSize(size,
|
||||||
|
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||||
|
}
|
||||||
|
await restorePos();
|
||||||
|
if (storeSize) {
|
||||||
|
await windowManager.setSize(size,
|
||||||
|
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (storeSize) {
|
||||||
|
await windowManager.setSize(size,
|
||||||
|
ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
|
||||||
|
}
|
||||||
|
await restorePos();
|
||||||
}
|
}
|
||||||
await restorePos();
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
restoreFrame() async {
|
restoreFrame() async {
|
||||||
if (offsetDevicePixelRatio == null) {
|
if (offsetLeftTop == null) {
|
||||||
await wc.center();
|
await wc.center();
|
||||||
} else {
|
} else {
|
||||||
final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
|
final frame = Rect.fromLTWH(
|
||||||
offsetDevicePixelRatio.offset.dy, size.width, size.height);
|
offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height);
|
||||||
await wc.setFrame(frame);
|
await wc.setFrame(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1964,6 +2016,8 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var webInitialLink = "";
|
||||||
|
|
||||||
/// Initialize uni links for macos/windows
|
/// Initialize uni links for macos/windows
|
||||||
///
|
///
|
||||||
/// [Availability]
|
/// [Availability]
|
||||||
@@ -1980,7 +2034,12 @@ Future<bool> initUniLinks() async {
|
|||||||
if (initialLink == null || initialLink.isEmpty) {
|
if (initialLink == null || initialLink.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return handleUriLink(uriString: initialLink);
|
if (isWeb) {
|
||||||
|
webInitialLink = initialLink;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return handleUriLink(uriString: initialLink);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugPrintStack(label: "$err");
|
debugPrintStack(label: "$err");
|
||||||
return false;
|
return false;
|
||||||
@@ -1993,7 +2052,7 @@ Future<bool> initUniLinks() async {
|
|||||||
///
|
///
|
||||||
/// Returns a [StreamSubscription] which can listen the uni links.
|
/// Returns a [StreamSubscription] which can listen the uni links.
|
||||||
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
|
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
|
||||||
if (isLinux) {
|
if (isLinux || isWeb) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2186,7 +2245,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = uri.queryParameters["key"];
|
var queryParameters =
|
||||||
|
uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
|
||||||
|
|
||||||
|
var key = queryParameters["key"];
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
id = "$id?key=$key";
|
id = "$id?key=$key";
|
||||||
@@ -2195,7 +2257,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
|||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
final forceRelay = uri.queryParameters["relay"] != null;
|
final forceRelay = queryParameters["relay"] != null;
|
||||||
connect(Get.context!, id, forceRelay: forceRelay);
|
connect(Get.context!, id, forceRelay: forceRelay);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2205,7 +2267,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
|||||||
if (command != null && id != null) {
|
if (command != null && id != null) {
|
||||||
args.add(command);
|
args.add(command);
|
||||||
args.add(id);
|
args.add(id);
|
||||||
var param = uri.queryParameters;
|
var param = queryParameters;
|
||||||
String? password = param["password"];
|
String? password = param["password"];
|
||||||
if (password != null) args.addAll(['--password', password]);
|
if (password != null) args.addAll(['--password', password]);
|
||||||
String? switch_uuid = param["switch_uuid"];
|
String? switch_uuid = param["switch_uuid"];
|
||||||
@@ -2223,16 +2285,19 @@ connectMainDesktop(String id,
|
|||||||
required bool isRDP,
|
required bool isRDP,
|
||||||
bool? forceRelay,
|
bool? forceRelay,
|
||||||
String? password,
|
String? password,
|
||||||
|
String? connToken,
|
||||||
bool? isSharedPassword}) async {
|
bool? isSharedPassword}) async {
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
await rustDeskWinManager.newFileTransfer(id,
|
await rustDeskWinManager.newFileTransfer(id,
|
||||||
password: password,
|
password: password,
|
||||||
isSharedPassword: isSharedPassword,
|
isSharedPassword: isSharedPassword,
|
||||||
|
connToken: connToken,
|
||||||
forceRelay: forceRelay);
|
forceRelay: forceRelay);
|
||||||
} else if (isTcpTunneling || isRDP) {
|
} else if (isTcpTunneling || isRDP) {
|
||||||
await rustDeskWinManager.newPortForward(id, isRDP,
|
await rustDeskWinManager.newPortForward(id, isRDP,
|
||||||
password: password,
|
password: password,
|
||||||
isSharedPassword: isSharedPassword,
|
isSharedPassword: isSharedPassword,
|
||||||
|
connToken: connToken,
|
||||||
forceRelay: forceRelay);
|
forceRelay: forceRelay);
|
||||||
} else {
|
} else {
|
||||||
await rustDeskWinManager.newRemoteDesktop(id,
|
await rustDeskWinManager.newRemoteDesktop(id,
|
||||||
@@ -2252,6 +2317,7 @@ connect(BuildContext context, String id,
|
|||||||
bool isRDP = false,
|
bool isRDP = false,
|
||||||
bool forceRelay = false,
|
bool forceRelay = false,
|
||||||
String? password,
|
String? password,
|
||||||
|
String? connToken,
|
||||||
bool? isSharedPassword}) async {
|
bool? isSharedPassword}) async {
|
||||||
if (id == '') return;
|
if (id == '') return;
|
||||||
if (!isDesktop || desktopType == DesktopType.main) {
|
if (!isDesktop || desktopType == DesktopType.main) {
|
||||||
@@ -2293,24 +2359,40 @@ connect(BuildContext context, String id,
|
|||||||
'password': password,
|
'password': password,
|
||||||
'isSharedPassword': isSharedPassword,
|
'isSharedPassword': isSharedPassword,
|
||||||
'forceRelay': forceRelay,
|
'forceRelay': forceRelay,
|
||||||
|
'connToken': connToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
if (isAndroid) {
|
||||||
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
return;
|
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Navigator.push(
|
if (isWeb) {
|
||||||
context,
|
Navigator.push(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (BuildContext context) => FileManagerPage(
|
MaterialPageRoute(
|
||||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
builder: (BuildContext context) =>
|
||||||
),
|
desktop_file_manager.FileManagerPage(
|
||||||
);
|
id: id,
|
||||||
|
password: password,
|
||||||
|
isSharedPassword: isSharedPassword),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (BuildContext context) => FileManagerPage(
|
||||||
|
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isWebDesktop) {
|
if (isWeb) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -2334,6 +2416,7 @@ connect(BuildContext context, String id,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stateGlobal.isInMainPage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||||
@@ -2418,9 +2501,20 @@ Future<void> onActiveWindowChanged() async {
|
|||||||
// But the app will not close.
|
// But the app will not close.
|
||||||
//
|
//
|
||||||
// No idea why we need to delay here, `terminate()` itself is also an async function.
|
// No idea why we need to delay here, `terminate()` itself is also an async function.
|
||||||
Future.delayed(Duration.zero, () {
|
//
|
||||||
RdPlatformChannel.instance.terminate();
|
// A quick workaround, use `Timer.periodic` to avoid the app not closing.
|
||||||
});
|
// Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()`
|
||||||
|
// may not work since `Flutter 3.24.4`, see the following logs.
|
||||||
|
// A delay will allow the app to close.
|
||||||
|
//
|
||||||
|
//```
|
||||||
|
// embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid.
|
||||||
|
// 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2)
|
||||||
|
// embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle.
|
||||||
|
// 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2).
|
||||||
|
// ```
|
||||||
|
periodic_immediate(
|
||||||
|
Duration(milliseconds: 30), RdPlatformChannel.instance.terminate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2513,7 +2607,7 @@ class ServerConfig {
|
|||||||
config['relay'] = relayServer.trim();
|
config['relay'] = relayServer.trim();
|
||||||
config['api'] = apiServer.trim();
|
config['api'] = apiServer.trim();
|
||||||
config['key'] = key.trim();
|
config['key'] = key.trim();
|
||||||
return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits))
|
return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits))
|
||||||
.split('')
|
.split('')
|
||||||
.reversed
|
.reversed
|
||||||
.join();
|
.join();
|
||||||
@@ -2636,30 +2730,6 @@ Future<bool> osxRequestAudio() async {
|
|||||||
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
|
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
|
||||||
}
|
}
|
||||||
|
|
||||||
class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
|
|
||||||
/// Creates scroll physics that does not let the user scroll.
|
|
||||||
const DraggableNeverScrollableScrollPhysics({super.parent});
|
|
||||||
|
|
||||||
@override
|
|
||||||
DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
|
||||||
return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldAcceptUserOffset(ScrollMetrics position) {
|
|
||||||
// TODO: find a better solution to check if the offset change is caused by the scrollbar.
|
|
||||||
// Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity].
|
|
||||||
if (position is ScrollPositionWithSingleContext) {
|
|
||||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
|
||||||
return position.activity is IdleScrollActivity;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get allowImplicitScrolling => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget futureBuilder(
|
Widget futureBuilder(
|
||||||
{required Future? future, required Widget Function(dynamic data) hasData}) {
|
{required Future? future, required Widget Function(dynamic data) hasData}) {
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
@@ -2739,7 +2809,7 @@ Widget buildRemoteBlock(
|
|||||||
onExit: (event) => block.value = false,
|
onExit: (event) => block.value = false,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
// scope block tab
|
// scope block tab
|
||||||
FocusScope(child: child, canRequestFocus: !block.value),
|
preventMouseKeyBuilder(child: child, block: block.value),
|
||||||
// mask block click, cm not block click and still use check_click_time to avoid block local click
|
// mask block click, cm not block click and still use check_click_time to avoid block local click
|
||||||
if (mask)
|
if (mask)
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -2751,6 +2821,11 @@ Widget buildRemoteBlock(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget preventMouseKeyBuilder({required Widget child, required bool block}) {
|
||||||
|
return ExcludeFocus(
|
||||||
|
excluding: block, child: AbsorbPointer(child: child, absorbing: block));
|
||||||
|
}
|
||||||
|
|
||||||
Widget unreadMessageCountBuilder(RxInt? count,
|
Widget unreadMessageCountBuilder(RxInt? count,
|
||||||
{double? size, double? fontSize}) {
|
{double? size, double? fontSize}) {
|
||||||
return Obx(() => Offstage(
|
return Obx(() => Offstage(
|
||||||
@@ -2814,7 +2889,7 @@ Widget buildErrorBanner(BuildContext context,
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: translate(err.value),
|
message: translate(err.value),
|
||||||
child: Text(
|
child: SelectableText(
|
||||||
translate(err.value),
|
translate(err.value),
|
||||||
),
|
),
|
||||||
)).marginSymmetric(vertical: 2),
|
)).marginSymmetric(vertical: 2),
|
||||||
@@ -2935,6 +3010,16 @@ openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
|||||||
final displays = i == kAllDisplayValue
|
final displays = i == kAllDisplayValue
|
||||||
? List.generate(pi.displays.length, (index) => index)
|
? List.generate(pi.displays.length, (index) => index)
|
||||||
: [i];
|
: [i];
|
||||||
|
// Try clear image model before switching from all displays
|
||||||
|
// 1. The remote side has multiple displays.
|
||||||
|
// 2. Do not use texture render.
|
||||||
|
// 3. Connect to Display 1.
|
||||||
|
// 4. Switch to multi-displays `kAllDisplayValue`
|
||||||
|
// 5. Switch to Display 2.
|
||||||
|
// Then the remote page will display last picture of Display 1 at the beginning.
|
||||||
|
if (pi.forceTextureRender && i != kAllDisplayValue) {
|
||||||
|
ffi.imageModel.clearImage();
|
||||||
|
}
|
||||||
bind.sessionSwitchDisplay(
|
bind.sessionSwitchDisplay(
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
sessionId: ffi.sessionId,
|
sessionId: ffi.sessionId,
|
||||||
@@ -2968,11 +3053,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
|||||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewConnectWindowFrame(
|
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
|
||||||
int windowId, String peerId, int? display, Rect? screenRect) async {
|
int? display, Rect? screenRect) async {
|
||||||
if (screenRect == null) {
|
if (screenRect == null) {
|
||||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
// Do not restore window position to new connection if there's a pre-session.
|
||||||
windowId: windowId, display: display, peerId: peerId);
|
// https://github.com/rustdesk/rustdesk/discussions/8825
|
||||||
|
if (preSessionCount == 0) {
|
||||||
|
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||||
|
windowId: windowId, display: display, peerId: peerId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await tryMoveToScreenAndSetFullscreen(screenRect);
|
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||||
}
|
}
|
||||||
@@ -3069,9 +3158,13 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
|
|||||||
|
|
||||||
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
||||||
String? text) {
|
String? text) {
|
||||||
|
text = text?.trim();
|
||||||
if (text != null && text.isNotEmpty) {
|
if (text != null && text.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final sc = ServerConfig.decode(text);
|
final sc = ServerConfig.decode(text);
|
||||||
|
if (isWeb || isIOS) {
|
||||||
|
sc.relayServer = '';
|
||||||
|
}
|
||||||
if (sc.idServer.isNotEmpty) {
|
if (sc.idServer.isNotEmpty) {
|
||||||
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
||||||
success.then((value) {
|
success.then((value) {
|
||||||
@@ -3316,6 +3409,42 @@ bool isInHomePage() {
|
|||||||
return controller.state.value.selected == 0;
|
return controller.state.value.selected == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPresetPasswordWarning() {
|
||||||
|
if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) !=
|
||||||
|
'N') {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
color: Colors.yellow,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
child: Text(
|
||||||
|
translate("Security Alert"),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize:
|
||||||
|
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildPresetPasswordWarningMobile() {
|
||||||
|
if (bind.isPresetPasswordMobileOnly()) {
|
||||||
|
return _buildPresetPasswordWarning();
|
||||||
|
} else {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildPresetPasswordWarning() {
|
Widget buildPresetPasswordWarning() {
|
||||||
return FutureBuilder<bool>(
|
return FutureBuilder<bool>(
|
||||||
future: bind.isPresetPassword(),
|
future: bind.isPresetPassword(),
|
||||||
@@ -3326,32 +3455,7 @@ Widget buildPresetPasswordWarning() {
|
|||||||
return Text(
|
return Text(
|
||||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||||
} else if (snapshot.hasData && snapshot.data == true) {
|
} else if (snapshot.hasData && snapshot.data == true) {
|
||||||
if (bind.mainGetBuildinOption(
|
return _buildPresetPasswordWarning();
|
||||||
key: kOptionRemovePresetPasswordWarning) !=
|
|
||||||
'N') {
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Container(
|
|
||||||
color: Colors.yellow,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
child: Text(
|
|
||||||
translate("Security Alert"),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontSize:
|
|
||||||
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
|
||||||
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 {
|
} else {
|
||||||
return SizedBox
|
return SizedBox
|
||||||
.shrink(); // Show nothing if the Future completed with false or null
|
.shrink(); // Show nothing if the Future completed with false or null
|
||||||
@@ -3405,7 +3509,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
|
get windowResizeEdgeSize =>
|
||||||
|
isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
|
||||||
|
|
||||||
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
|
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
|
||||||
// See _linuxWindowResizable for more details.
|
// See _linuxWindowResizable for more details.
|
||||||
@@ -3423,7 +3528,12 @@ setResizable(bool resizable) {
|
|||||||
|
|
||||||
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
||||||
|
|
||||||
final isCustomClient = bind.isCustomClient();
|
bool? _isCustomClient;
|
||||||
|
bool get isCustomClient {
|
||||||
|
_isCustomClient ??= bind.isCustomClient();
|
||||||
|
return _isCustomClient!;
|
||||||
|
}
|
||||||
|
|
||||||
get defaultOptionLang => isCustomClient ? 'default' : '';
|
get defaultOptionLang => isCustomClient ? 'default' : '';
|
||||||
get defaultOptionTheme => isCustomClient ? 'system' : '';
|
get defaultOptionTheme => isCustomClient ? 'system' : '';
|
||||||
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
||||||
@@ -3461,3 +3571,70 @@ disableWindowMovable(int? windowId) {
|
|||||||
WindowController.fromWindowId(windowId).setMovable(false);
|
WindowController.fromWindowId(windowId).setMovable(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget netWorkErrorWidget() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(translate("network_error_tip")),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: gFFI.userModel.refreshCurrentUser,
|
||||||
|
child: Text(translate("Retry")))
|
||||||
|
.marginSymmetric(vertical: 16),
|
||||||
|
SelectableText(gFFI.userModel.networkError.value,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.red)),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResizeEdge>? get windowManagerEnableResizeEdges => isWindows
|
||||||
|
? [
|
||||||
|
ResizeEdge.topLeft,
|
||||||
|
ResizeEdge.top,
|
||||||
|
ResizeEdge.topRight,
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
|
||||||
|
? [
|
||||||
|
SubWindowResizeEdge.topLeft,
|
||||||
|
SubWindowResizeEdge.top,
|
||||||
|
SubWindowResizeEdge.topRight,
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
void earlyAssert() {
|
||||||
|
assert('\1' == '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkUpdate() {
|
||||||
|
if (!isWeb) {
|
||||||
|
if (!bind.isCustomClient()) {
|
||||||
|
platformFFI.registerEventHandler(
|
||||||
|
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||||
|
(Map<String, dynamic> evt) async {
|
||||||
|
if (evt['url'] is String) {
|
||||||
|
stateGlobal.updateUrl.value = evt['url'];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Timer(const Duration(seconds: 1), () async {
|
||||||
|
bind.mainGetSoftwareUpdateUrl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/flutter/flutter/issues/153560#issuecomment-2497160535
|
||||||
|
// For TextField, TextFormField
|
||||||
|
extension WorkaroundFreezeLinuxMint on Widget {
|
||||||
|
Widget workaroundFreezeLinuxMint() {
|
||||||
|
// No need to check if is Linux Mint, because this workaround is harmless on other platforms.
|
||||||
|
if (isLinux) {
|
||||||
|
return ExcludeSemantics(child: this);
|
||||||
|
} else {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -11,6 +12,7 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||||
import 'package:flutter_hbb/models/ab_model.dart';
|
import 'package:flutter_hbb/models/ab_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_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 'package:url_launcher/url_launcher_string.dart';
|
||||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -35,17 +37,14 @@ class AddressBook extends StatefulWidget {
|
|||||||
class _AddressBookState extends State<AddressBook> {
|
class _AddressBookState extends State<AddressBook> {
|
||||||
var menuPos = RelativeRect.fill;
|
var menuPos = RelativeRect.fill;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Obx(() {
|
Widget build(BuildContext context) => Obx(() {
|
||||||
if (!gFFI.userModel.isLogin) {
|
if (!gFFI.userModel.isLogin) {
|
||||||
return Center(
|
return Center(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||||
|
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||||
|
return netWorkErrorWidget();
|
||||||
} else {
|
} else {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -64,15 +63,16 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
retry: null, // remove retry
|
retry: null, // remove retry
|
||||||
close: () => gFFI.abModel.currentAbPushError.value = ''),
|
close: () => gFFI.abModel.currentAbPushError.value = ''),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: (isDesktop || isWebDesktop)
|
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||||
? _buildAddressBookDesktop()
|
? _buildAddressBookPortrait()
|
||||||
: _buildAddressBookMobile())
|
: _buildAddressBookLandscape()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildAddressBookDesktop() {
|
Widget _buildAddressBookLandscape() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -109,7 +109,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAddressBookMobile() {
|
Widget _buildAddressBookPortrait() {
|
||||||
const padding = 8.0;
|
const padding = 8.0;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -242,14 +242,15 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
customButton: Container(
|
customButton: Obx(() => Container(
|
||||||
height: isDesktop ? 48 : 40,
|
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
child:
|
||||||
Icon(Icons.arrow_drop_down),
|
buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||||
]),
|
Icon(Icons.arrow_drop_down),
|
||||||
),
|
]),
|
||||||
|
)),
|
||||||
underline: Container(
|
underline: Container(
|
||||||
height: 0.7,
|
height: 0.7,
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
@@ -285,7 +286,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
searchMatchFn: (item, searchValue) {
|
searchMatchFn: (item, searchValue) {
|
||||||
return item.value
|
return item.value
|
||||||
@@ -316,13 +317,14 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
|
|
||||||
Widget _buildTags() {
|
Widget _buildTags() {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final List tags;
|
List tags;
|
||||||
if (gFFI.abModel.sortTags.value) {
|
if (gFFI.abModel.sortTags.value) {
|
||||||
tags = gFFI.abModel.currentAbTags.toList();
|
tags = gFFI.abModel.currentAbTags.toList();
|
||||||
tags.sort();
|
tags.sort();
|
||||||
} else {
|
} else {
|
||||||
tags = gFFI.abModel.currentAbTags;
|
tags = gFFI.abModel.currentAbTags.toList();
|
||||||
}
|
}
|
||||||
|
tags = [kUntagged, ...tags].toList();
|
||||||
final editPermission = gFFI.abModel.current.canWrite();
|
final editPermission = gFFI.abModel.current.canWrite();
|
||||||
tagBuilder(String e) {
|
tagBuilder(String e) {
|
||||||
return AddressBookTag(
|
return AddressBookTag(
|
||||||
@@ -338,8 +340,8 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
showActionMenu: editPermission);
|
showActionMenu: editPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
final gridView = DynamicGridView.builder(
|
gridView(bool isPortrait) => DynamicGridView.builder(
|
||||||
shrinkWrap: isMobile,
|
shrinkWrap: isPortrait,
|
||||||
gridDelegate: SliverGridDelegateWithWrapping(),
|
gridDelegate: SliverGridDelegateWithWrapping(),
|
||||||
itemCount: tags.length,
|
itemCount: tags.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
@@ -347,9 +349,9 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return tagBuilder(e);
|
return tagBuilder(e);
|
||||||
});
|
});
|
||||||
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||||
return (isDesktop || isWebDesktop)
|
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||||
? gridView
|
? gridView(false)
|
||||||
: LimitedBox(maxHeight: maxHeight, child: gridView);
|
: LimitedBox(maxHeight: maxHeight, child: gridView(true)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +361,6 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: AddressBookPeersView(
|
child: AddressBookPeersView(
|
||||||
menuPadding: widget.menuPadding,
|
menuPadding: widget.menuPadding,
|
||||||
getInitPeers: () => gFFI.abModel.currentAbPeers,
|
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -425,7 +426,8 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
|
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
|
||||||
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
||||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||||
sortMenuItem(),
|
if (gFFI.abModel.legacyMode.value)
|
||||||
|
sortMenuItem(), // It's already sorted after pulling down
|
||||||
if (canWrite) syncMenuItem(),
|
if (canWrite) syncMenuItem(),
|
||||||
filterMenuItem(),
|
filterMenuItem(),
|
||||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||||
@@ -508,20 +510,21 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
double marginBottom = 4;
|
double marginBottom = 4;
|
||||||
|
|
||||||
row({required Widget lable, required Widget input}) {
|
row({required Widget lable, required Widget input}) {
|
||||||
return Row(
|
makeChild(bool isPortrait) => Row(
|
||||||
children: [
|
children: [
|
||||||
!isMobile
|
!isPortrait
|
||||||
? ConstrainedBox(
|
? ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 100),
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
child: lable.marginOnly(right: 10))
|
child: lable.marginOnly(right: 10))
|
||||||
: SizedBox.shrink(),
|
: SizedBox.shrink(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 200),
|
constraints: const BoxConstraints(minWidth: 200),
|
||||||
child: input),
|
child: input),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: !isMobile ? 8 : 0);
|
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||||
|
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
@@ -544,24 +547,29 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
input: TextField(
|
input: Obx(() => TextField(
|
||||||
controller: idController,
|
controller: idController,
|
||||||
inputFormatters: [IDTextInputFormatter()],
|
inputFormatters: [IDTextInputFormatter()],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: !isMobile ? null : translate('ID'),
|
labelText: stateGlobal.isPortrait.isFalse
|
||||||
errorText: errorMsg,
|
? null
|
||||||
errorMaxLines: 5),
|
: translate('ID'),
|
||||||
)),
|
errorText: errorMsg,
|
||||||
|
errorMaxLines: 5),
|
||||||
|
).workaroundFreezeLinuxMint())),
|
||||||
row(
|
row(
|
||||||
lable: Text(
|
lable: Text(
|
||||||
translate('Alias'),
|
translate('Alias'),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
input: TextField(
|
input: Obx(() => TextField(
|
||||||
controller: aliasController,
|
controller: aliasController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: !isMobile ? null : translate('Alias'),
|
labelText: stateGlobal.isPortrait.isFalse
|
||||||
)),
|
? null
|
||||||
|
: translate('Alias'),
|
||||||
|
),
|
||||||
|
).workaroundFreezeLinuxMint()),
|
||||||
),
|
),
|
||||||
if (isCurrentAbShared)
|
if (isCurrentAbShared)
|
||||||
row(
|
row(
|
||||||
@@ -569,24 +577,28 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
translate('Password'),
|
translate('Password'),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
input: TextField(
|
input: Obx(
|
||||||
controller: passwordController,
|
() => TextField(
|
||||||
obscureText: !passwordVisible,
|
controller: passwordController,
|
||||||
decoration: InputDecoration(
|
obscureText: !passwordVisible,
|
||||||
labelText: !isMobile ? null : translate('Password'),
|
decoration: InputDecoration(
|
||||||
suffixIcon: IconButton(
|
labelText: stateGlobal.isPortrait.isFalse
|
||||||
icon: Icon(
|
? null
|
||||||
passwordVisible
|
: translate('Password'),
|
||||||
? Icons.visibility
|
suffixIcon: IconButton(
|
||||||
: Icons.visibility_off,
|
icon: Icon(
|
||||||
color: MyTheme.lightTheme.primaryColor),
|
passwordVisible
|
||||||
onPressed: () {
|
? Icons.visibility
|
||||||
setState(() {
|
: Icons.visibility_off,
|
||||||
passwordVisible = !passwordVisible;
|
color: MyTheme.lightTheme.primaryColor),
|
||||||
});
|
onPressed: () {
|
||||||
},
|
setState(() {
|
||||||
|
passwordVisible = !passwordVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
)),
|
)),
|
||||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||||
Align(
|
Align(
|
||||||
@@ -659,6 +671,14 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
} else {
|
} else {
|
||||||
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||||
field = tags.join(',');
|
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);
|
gFFI.abModel.addTags(tags);
|
||||||
// final currentPeers
|
// final currentPeers
|
||||||
}
|
}
|
||||||
@@ -684,7 +704,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
),
|
),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -731,12 +751,14 @@ class AddressBookTag extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const double radius = 8;
|
const double radius = 8;
|
||||||
|
final isUnTagged = name == kUntagged;
|
||||||
|
final showAction = showActionMenu && !isUnTagged;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onTapDown: showActionMenu ? setPosition : null,
|
onTapDown: showAction ? setPosition : null,
|
||||||
onSecondaryTapDown: showActionMenu ? setPosition : null,
|
onSecondaryTapDown: showAction ? setPosition : null,
|
||||||
onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null,
|
onSecondaryTap: showAction ? () => _showMenu(context, pos) : null,
|
||||||
onLongPress: showActionMenu ? () => _showMenu(context, pos) : null,
|
onLongPress: showAction ? () => _showMenu(context, pos) : null,
|
||||||
child: Obx(() => Container(
|
child: Obx(() => Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: tags.contains(name)
|
color: tags.contains(name)
|
||||||
@@ -748,17 +770,18 @@ class AddressBookTag extends StatelessWidget {
|
|||||||
child: IntrinsicWidth(
|
child: IntrinsicWidth(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
if (!isUnTagged)
|
||||||
width: radius,
|
Container(
|
||||||
height: radius,
|
width: radius,
|
||||||
decoration: BoxDecoration(
|
height: radius,
|
||||||
shape: BoxShape.circle,
|
decoration: BoxDecoration(
|
||||||
color: tags.contains(name)
|
shape: BoxShape.circle,
|
||||||
? Colors.white
|
color: tags.contains(name)
|
||||||
: gFFI.abModel.getCurrentAbTagColor(name)),
|
? Colors.white
|
||||||
).marginOnly(right: radius / 2),
|
: gFFI.abModel.getCurrentAbTagColor(name)),
|
||||||
|
).marginOnly(right: radius / 2),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(name,
|
child: Text(isUnTagged ? translate(name) : name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
color: tags.contains(name) ? Colors.white : null)),
|
color: tags.contains(name) ? Colors.white : null)),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
|
||||||
const _kWindowsSystemSound = 'System Sound';
|
const _kSystemSound = 'System Sound';
|
||||||
|
|
||||||
typedef AudioINputSetDevice = void Function(String device);
|
typedef AudioINputSetDevice = void Function(String device);
|
||||||
typedef AudioInputBuilder = Widget Function(
|
typedef AudioInputBuilder = Widget Function(
|
||||||
@@ -21,7 +21,7 @@ class AudioInput extends StatelessWidget {
|
|||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
static String getDefault() {
|
static String getDefault() {
|
||||||
if (isWindows) return translate('System Sound');
|
if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ class AudioInput extends StatelessWidget {
|
|||||||
static Future<Map<String, Object>> getDevicesInfo(
|
static Future<Map<String, Object>> getDevicesInfo(
|
||||||
bool isCm, bool isVoiceCall) async {
|
bool isCm, bool isVoiceCall) async {
|
||||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||||
if (isWindows) {
|
if (bind.mainAudioSupportLoopback()) {
|
||||||
devices.insert(0, translate(_kWindowsSystemSound));
|
devices.insert(0, translate(_kSystemSound));
|
||||||
}
|
}
|
||||||
String current = await getValue(isCm, isVoiceCall);
|
String current = await getValue(isCm, isVoiceCall);
|
||||||
return {'devices': devices, 'current': current};
|
return {'devices': devices, 'current': current};
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
|
|||||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||||
.toList();
|
.toList();
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: isMobile
|
message: !(isDesktop || isWebDesktop)
|
||||||
? ''
|
? ''
|
||||||
: widget.peer.tags.isNotEmpty
|
: widget.peer.tags.isNotEmpty
|
||||||
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
|
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class ChatPage extends StatelessWidget implements PageShape {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
).workaroundFreezeLinuxMint();
|
||||||
return SelectionArea(child: chat);
|
return SelectionArea(child: chat);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
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');
|
String get name => translate('uppercase');
|
||||||
@override
|
@override
|
||||||
bool validate(String value) {
|
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
|
@override
|
||||||
bool validate(String value) {
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
|||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/peer_tab_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:get/get.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
|
||||||
@@ -139,7 +140,7 @@ void changeIdDialog() {
|
|||||||
msg = '';
|
msg = '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8.0,
|
height: 8.0,
|
||||||
),
|
),
|
||||||
@@ -200,13 +201,14 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
errorText: msg.isEmpty ? null : translate(msg),
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
),
|
),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: !isOptFixed,
|
enabled: !isOptFixed,
|
||||||
autofocus: true),
|
autofocus: true)
|
||||||
|
.workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -286,22 +288,23 @@ Future<String> changeDirectAccessPort(
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '21118',
|
hintText: '21118',
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
prefix: Text('$currentIP : '),
|
prefix: Text('$currentIP : '),
|
||||||
suffix: IconButton(
|
suffix: IconButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
icon: const Icon(Icons.clear, size: 16),
|
icon: const Icon(Icons.clear, size: 16),
|
||||||
onPressed: () => controller.clear())),
|
onPressed: () => controller.clear())),
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(RegExp(
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
|
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
|
||||||
],
|
],
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true),
|
autofocus: true)
|
||||||
|
.workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -334,21 +337,22 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '10',
|
hintText: '10',
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
suffix: IconButton(
|
suffix: IconButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
icon: const Icon(Icons.clear, size: 16),
|
icon: const Icon(Icons.clear, size: 16),
|
||||||
onPressed: () => controller.clear())),
|
onPressed: () => controller.clear())),
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(RegExp(
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
|
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
|
||||||
],
|
],
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true),
|
autofocus: true)
|
||||||
|
.workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -380,6 +384,7 @@ class DialogTextField extends StatelessWidget {
|
|||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
final TextInputType? keyboardType;
|
final TextInputType? keyboardType;
|
||||||
final List<TextInputFormatter>? inputFormatters;
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
final int? maxLength;
|
||||||
|
|
||||||
static const kUsernameTitle = 'Username';
|
static const kUsernameTitle = 'Username';
|
||||||
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
||||||
@@ -397,6 +402,7 @@ class DialogTextField extends StatelessWidget {
|
|||||||
this.hintText,
|
this.hintText,
|
||||||
this.keyboardType,
|
this.keyboardType,
|
||||||
this.inputFormatters,
|
this.inputFormatters,
|
||||||
|
this.maxLength,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.controller})
|
required this.controller})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -423,7 +429,8 @@ class DialogTextField extends StatelessWidget {
|
|||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
),
|
maxLength: maxLength,
|
||||||
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(vertical: 4.0);
|
).paddingSymmetric(vertical: 4.0);
|
||||||
@@ -679,6 +686,8 @@ class PasswordWidget extends StatefulWidget {
|
|||||||
this.reRequestFocus = false,
|
this.reRequestFocus = false,
|
||||||
this.hintText,
|
this.hintText,
|
||||||
this.errorText,
|
this.errorText,
|
||||||
|
this.title,
|
||||||
|
this.maxLength,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
@@ -686,6 +695,8 @@ class PasswordWidget extends StatefulWidget {
|
|||||||
final bool reRequestFocus;
|
final bool reRequestFocus;
|
||||||
final String? hintText;
|
final String? hintText;
|
||||||
final String? errorText;
|
final String? errorText;
|
||||||
|
final String? title;
|
||||||
|
final int? maxLength;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PasswordWidget> createState() => _PasswordWidgetState();
|
State<PasswordWidget> createState() => _PasswordWidgetState();
|
||||||
@@ -729,7 +740,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DialogTextField(
|
return DialogTextField(
|
||||||
title: translate(DialogTextField.kPasswordTitle),
|
title: translate(widget.title ?? DialogTextField.kPasswordTitle),
|
||||||
hintText: translate(widget.hintText ?? 'Enter your password'),
|
hintText: translate(widget.hintText ?? 'Enter your password'),
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
prefixIcon: DialogTextField.kPasswordIcon,
|
prefixIcon: DialogTextField.kPasswordIcon,
|
||||||
@@ -748,6 +759,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
|||||||
obscureText: !_passwordVisible,
|
obscureText: !_passwordVisible,
|
||||||
errorText: widget.errorText,
|
errorText: widget.errorText,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
|
maxLength: widget.maxLength,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1121,7 +1133,7 @@ void showRequestElevationDialog(
|
|||||||
errorText: errPwd.isEmpty ? null : errPwd.value,
|
errorText: errPwd.isEmpty ? null : errPwd.value,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
|
).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
|
||||||
).marginOnly(top: 10),
|
).marginOnly(top: 10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1492,7 +1504,7 @@ showAuditDialog(FFI ffi) async {
|
|||||||
maxLength: 256,
|
maxLength: 256,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
)),
|
).workaroundFreezeLinuxMint()),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||||
dialogButton('OK', onPressed: submit)
|
dialogButton('OK', onPressed: submit)
|
||||||
@@ -1739,7 +1751,7 @@ void renameDialog(
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(labelText: translate('Name')),
|
decoration: InputDecoration(labelText: translate('Name')),
|
||||||
validator: validator,
|
validator: validator,
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// NOT use Offstage to wrap LinearProgressIndicator
|
// NOT use Offstage to wrap LinearProgressIndicator
|
||||||
@@ -1799,7 +1811,7 @@ void changeBot({Function()? callback}) async {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: translate('Token'),
|
hintText: translate('Token'),
|
||||||
),
|
),
|
||||||
);
|
).workaroundFreezeLinuxMint();
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Telegram bot")),
|
title: Text(translate("Telegram bot")),
|
||||||
@@ -1829,6 +1841,7 @@ void changeBot({Function()? callback}) async {
|
|||||||
void change2fa({Function()? callback}) async {
|
void change2fa({Function()? callback}) async {
|
||||||
if (bind.mainHasValid2FaSync()) {
|
if (bind.mainHasValid2FaSync()) {
|
||||||
await bind.mainSetOption(key: "2fa", value: "");
|
await bind.mainSetOption(key: "2fa", value: "");
|
||||||
|
await bind.mainClearTrustedDevices();
|
||||||
callback?.call();
|
callback?.call();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1896,6 +1909,7 @@ void enter2FaDialog(
|
|||||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
final RxBool submitReady = false.obs;
|
final RxBool submitReady = false.obs;
|
||||||
|
final RxBool trustThisDevice = false.obs;
|
||||||
|
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
dialogManager.show((setState, close, context) {
|
dialogManager.show((setState, close, context) {
|
||||||
@@ -1905,7 +1919,7 @@ void enter2FaDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
gFFI.send2FA(sessionId, controller.text.trim());
|
gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
|
||||||
close();
|
close();
|
||||||
dialogManager.showLoading(translate('Logging in...'),
|
dialogManager.showLoading(translate('Logging in...'),
|
||||||
onCancel: closeConnection);
|
onCancel: closeConnection);
|
||||||
@@ -1919,9 +1933,27 @@ void enter2FaDialog(
|
|||||||
onChanged: () => submitReady.value = codeField.isReady,
|
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(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('enter-2fa-title')),
|
title: Text(translate('enter-2fa-title')),
|
||||||
content: codeField,
|
content: Column(
|
||||||
|
children: [
|
||||||
|
codeField,
|
||||||
|
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
|
||||||
|
trustField,
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton('Cancel',
|
dialogButton('Cancel',
|
||||||
onPressed: cancel,
|
onPressed: cancel,
|
||||||
@@ -2149,7 +2181,7 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
if (!gFFI.abModel.current.isPersonal())
|
if (!gFFI.abModel.current.isPersonal())
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
|
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
|
||||||
@@ -2216,3 +2248,255 @@ void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// end
|
||||||
switch (_currentState) {
|
switch (_currentState) {
|
||||||
case GestureState.oneFingerPan:
|
case GestureState.oneFingerPan:
|
||||||
debugPrint("TwoFingerState.pan onEnd");
|
debugPrint("OneFingerState.pan onEnd");
|
||||||
if (onOneFingerPanEnd != null) {
|
if (onOneFingerPanEnd != null) {
|
||||||
onOneFingerPanEnd!(_getDragEndDetails(d));
|
onOneFingerPanEnd!(_getDragEndDetails(d));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
|
|||||||
String _failedMsg = '';
|
String _failedMsg = '';
|
||||||
String _url = '';
|
String _url = '';
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -683,7 +678,7 @@ Future<bool?> verificationCodeDialog(
|
|||||||
labelText: "Email", prefixIcon: Icon(Icons.email)),
|
labelText: "Email", prefixIcon: Icon(Icons.email)),
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
controller: TextEditingController(text: user?.email),
|
controller: TextEditingController(text: user?.email),
|
||||||
)),
|
).workaroundFreezeLinuxMint()),
|
||||||
isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
|
isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
|
||||||
codeField,
|
codeField,
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/login.dart';
|
import 'package:flutter_hbb/common/widgets/login.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
@@ -23,11 +24,6 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
RxString get searchUserText => gFFI.groupModel.searchUserText;
|
RxString get searchUserText => gFFI.groupModel.searchUserText;
|
||||||
static TextEditingController searchUserController = TextEditingController();
|
static TextEditingController searchUserController = TextEditingController();
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
@@ -35,6 +31,8 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
return Center(
|
return Center(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||||
|
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||||
|
return netWorkErrorWidget();
|
||||||
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
|
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
@@ -48,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
retry: null,
|
retry: null,
|
||||||
close: () => gFFI.groupModel.groupLoadError.value = ''),
|
close: () => gFFI.groupModel.groupLoadError.value = ''),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: (isDesktop || isWebDesktop)
|
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||||
? _buildDesktop()
|
? _buildPortrait()
|
||||||
: _buildMobile())
|
: _buildLandscape())),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDesktop() {
|
Widget _buildLandscape() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -85,14 +83,14 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: MyGroupPeerView(
|
child: MyGroupPeerView(
|
||||||
menuPadding: widget.menuPadding,
|
menuPadding: widget.menuPadding,
|
||||||
getInitPeers: () => gFFI.groupModel.peers)),
|
)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMobile() {
|
Widget _buildPortrait() {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -117,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: MyGroupPeerView(
|
child: MyGroupPeerView(
|
||||||
menuPadding: widget.menuPadding,
|
menuPadding: widget.menuPadding,
|
||||||
getInitPeers: () => gFFI.groupModel.peers)),
|
)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -147,7 +145,7 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
)),
|
).workaroundFreezeLinuxMint()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -162,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
final listView = ListView.builder(
|
listView(bool isPortrait) => ListView.builder(
|
||||||
shrinkWrap: isMobile,
|
shrinkWrap: isPortrait,
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
||||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||||
return (isDesktop || isWebDesktop)
|
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||||
? listView
|
? listView(false)
|
||||||
: LimitedBox(maxHeight: maxHeight, child: listView);
|
: LimitedBox(maxHeight: maxHeight, child: listView(true)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ class Draggable extends StatefulWidget {
|
|||||||
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _DraggableState();
|
State<StatefulWidget> createState() => _DraggableState(chatModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableState extends State<Draggable> {
|
class _DraggableState extends State<Draggable> {
|
||||||
@@ -362,10 +362,8 @@ class _DraggableState extends State<Draggable> {
|
|||||||
double _saveHeight = 0;
|
double _saveHeight = 0;
|
||||||
double _lastBottomHeight = 0;
|
double _lastBottomHeight = 0;
|
||||||
|
|
||||||
@override
|
_DraggableState(ChatModel? chatModel) {
|
||||||
void initState() {
|
_chatModel = chatModel;
|
||||||
super.initState();
|
|
||||||
_chatModel = widget.chatModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get position => widget.position.pos;
|
get position => widget.position.pos;
|
||||||
@@ -467,7 +465,8 @@ class IOSDraggable extends StatefulWidget {
|
|||||||
final Widget Function(BuildContext) builder;
|
final Widget Function(BuildContext) builder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IOSDraggableState createState() => IOSDraggableState();
|
IOSDraggableState createState() =>
|
||||||
|
IOSDraggableState(chatModel, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
class IOSDraggableState extends State<IOSDraggable> {
|
class IOSDraggableState extends State<IOSDraggable> {
|
||||||
@@ -478,12 +477,10 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
double _saveHeight = 0;
|
double _saveHeight = 0;
|
||||||
double _lastBottomHeight = 0;
|
double _lastBottomHeight = 0;
|
||||||
|
|
||||||
@override
|
IOSDraggableState(ChatModel? chatModel, double w, double h) {
|
||||||
void initState() {
|
_chatModel = chatModel;
|
||||||
super.initState();
|
_width = w;
|
||||||
_chatModel = widget.chatModel;
|
_height = h;
|
||||||
_width = widget.width;
|
|
||||||
_height = widget.height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DraggableKeyPosition get position => widget.position;
|
DraggableKeyPosition get position => widget.position;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/peer_tab_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:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -53,42 +54,44 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
if (isDesktop || isWebDesktop) {
|
return Obx(() =>
|
||||||
return _buildDesktop();
|
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
|
||||||
} else {
|
|
||||||
return _buildMobile();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMobile() {
|
Widget gestureDetector({required Widget child}) {
|
||||||
final peer = super.widget.peer;
|
|
||||||
final PeerTabModel peerTabModel = Provider.of(context);
|
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(
|
return Card(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 2),
|
margin: EdgeInsets.symmetric(horizontal: 2),
|
||||||
child: GestureDetector(
|
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: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
||||||
child: _buildPeerTile(context, peer, null)),
|
child: _buildPeerTile(context, peer, null)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDesktop() {
|
Widget _buildLandscape() {
|
||||||
final PeerTabModel peerTabModel = Provider.of(context);
|
|
||||||
final peer = super.widget.peer;
|
final peer = super.widget.peer;
|
||||||
var deco = Rx<BoxDecoration?>(
|
var deco = Rx<BoxDecoration?>(
|
||||||
BoxDecoration(
|
BoxDecoration(
|
||||||
@@ -117,36 +120,27 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: GestureDetector(
|
child: gestureDetector(
|
||||||
onDoubleTap:
|
|
||||||
peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
|
|
||||||
? null
|
|
||||||
: () => widget.connect(context, peer.id),
|
|
||||||
onTap: () => peerTabModel.select(peer),
|
|
||||||
onLongPress: () => peerTabModel.select(peer),
|
|
||||||
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
||||||
? _buildPeerCard(context, peer, deco)
|
? _buildPeerCard(context, peer, deco)
|
||||||
: _buildPeerTile(context, peer, deco))),
|
: _buildPeerTile(context, peer, deco))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPeerTile(
|
makeChild(bool isPortrait, Peer peer) {
|
||||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
|
||||||
hideUsernameOnCard ??=
|
|
||||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
|
||||||
final name = hideUsernameOnCard == true
|
final name = hideUsernameOnCard == true
|
||||||
? peer.hostname
|
? peer.hostname
|
||||||
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||||
final greyStyle = TextStyle(
|
final greyStyle = TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||||
final child = Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||||
borderRadius: isMobile
|
borderRadius: isPortrait
|
||||||
? BorderRadius.circular(_tileRadius)
|
? BorderRadius.circular(_tileRadius)
|
||||||
: BorderRadius.only(
|
: BorderRadius.only(
|
||||||
topLeft: Radius.circular(_tileRadius),
|
topLeft: Radius.circular(_tileRadius),
|
||||||
@@ -154,11 +148,11 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
width: isMobile ? 50 : 42,
|
width: isPortrait ? 50 : 42,
|
||||||
height: isMobile ? 50 : null,
|
height: isPortrait ? 50 : null,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
|
||||||
.paddingAll(6),
|
.paddingAll(6),
|
||||||
if (_shouldBuildPasswordIcon(peer))
|
if (_shouldBuildPasswordIcon(peer))
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -183,19 +177,19 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
getOnline(isMobile ? 4 : 8, peer.online),
|
getOnline(isPortrait ? 4 : 8, peer.online),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
)),
|
)),
|
||||||
]).marginOnly(top: isMobile ? 0 : 2),
|
]).marginOnly(top: isPortrait ? 0 : 2),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
name,
|
name,
|
||||||
style: isMobile ? null : greyStyle,
|
style: isPortrait ? null : greyStyle,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -203,41 +197,47 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
],
|
],
|
||||||
).marginOnly(top: 2),
|
).marginOnly(top: 2),
|
||||||
),
|
),
|
||||||
isMobile
|
isPortrait
|
||||||
? checkBoxOrActionMoreMobile(peer)
|
? checkBoxOrActionMorePortrait(peer)
|
||||||
: checkBoxOrActionMoreDesktop(peer, isTile: true),
|
: checkBoxOrActionMoreLandscape(peer, isTile: true),
|
||||||
],
|
],
|
||||||
).paddingOnly(left: 10.0, top: 3.0),
|
).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)
|
final colors = _frontN(peer.tags, 25)
|
||||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||||
.toList();
|
.toList();
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: isMobile
|
message: !(isDesktop || isWebDesktop)
|
||||||
? ''
|
? ''
|
||||||
: peer.tags.isNotEmpty
|
: peer.tags.isNotEmpty
|
||||||
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
||||||
: '',
|
: '',
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
deco == null
|
Obx(
|
||||||
? child
|
() => deco == null
|
||||||
: Obx(
|
? makeChild(stateGlobal.isPortrait.isTrue, peer)
|
||||||
() => Container(
|
: Container(
|
||||||
foregroundDecoration: deco.value,
|
foregroundDecoration: deco.value,
|
||||||
child: child,
|
child: makeChild(stateGlobal.isPortrait.isTrue, peer),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (colors.isNotEmpty)
|
if (colors.isNotEmpty)
|
||||||
Positioned(
|
Obx(() => Positioned(
|
||||||
top: 2,
|
top: 2,
|
||||||
right: isMobile ? 20 : 10,
|
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: TagPainter(radius: 3, colors: colors),
|
painter: TagPainter(radius: 3, colors: colors),
|
||||||
),
|
),
|
||||||
)
|
))
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -253,6 +253,9 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
margin: EdgeInsets.zero,
|
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(
|
child: Obx(
|
||||||
() => Container(
|
() => Container(
|
||||||
foregroundDecoration: deco.value,
|
foregroundDecoration: deco.value,
|
||||||
@@ -316,7 +319,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
)),
|
)),
|
||||||
]).paddingSymmetric(vertical: 8)),
|
]).paddingSymmetric(vertical: 8)),
|
||||||
checkBoxOrActionMoreDesktop(peer, isTile: false),
|
checkBoxOrActionMoreLandscape(peer, isTile: false),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 12.0),
|
).paddingSymmetric(horizontal: 12.0),
|
||||||
)
|
)
|
||||||
@@ -362,7 +365,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget checkBoxOrActionMoreMobile(Peer peer) {
|
Widget checkBoxOrActionMorePortrait(Peer peer) {
|
||||||
final PeerTabModel peerTabModel = Provider.of(context);
|
final PeerTabModel peerTabModel = Provider.of(context);
|
||||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||||
if (peerTabModel.multiSelectionMode) {
|
if (peerTabModel.multiSelectionMode) {
|
||||||
@@ -390,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 PeerTabModel peerTabModel = Provider.of(context);
|
||||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||||
if (peerTabModel.multiSelectionMode) {
|
if (peerTabModel.multiSelectionMode) {
|
||||||
@@ -636,8 +639,8 @@ abstract class BasePeerCard extends StatelessWidget {
|
|||||||
|
|
||||||
@protected
|
@protected
|
||||||
Future<bool> _isForceAlwaysRelay(String id) async {
|
Future<bool> _isForceAlwaysRelay(String id) async {
|
||||||
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))
|
return option2bool(kOptionForceAlwaysRelay,
|
||||||
.isNotEmpty;
|
(await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
@@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
|
|
||||||
final List favs = (await bind.mainGetFav()).toList();
|
final List favs = (await bind.mainGetFav()).toList();
|
||||||
@@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||||
menuItems.add(_tcpTunnelingAction(context));
|
menuItems.add(_tcpTunnelingAction(context));
|
||||||
@@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
|
|
||||||
final List favs = (await bind.mainGetFav()).toList();
|
final List favs = (await bind.mainGetFav()).toList();
|
||||||
@@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||||
menuItems.add(_tcpTunnelingAction(context));
|
menuItems.add(_tcpTunnelingAction(context));
|
||||||
@@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||||
menuItems.add(_tcpTunnelingAction(context));
|
menuItems.add(_tcpTunnelingAction(context));
|
||||||
@@ -1203,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _rdpDialog(String id) async {
|
void _rdpDialog(String id) async {
|
||||||
|
final maxLength = bind.mainMaxEncryptLen();
|
||||||
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
|
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
|
||||||
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
|
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
|
||||||
final portController = TextEditingController(text: port);
|
final portController = TextEditingController(text: port);
|
||||||
@@ -1253,58 +1257,58 @@ void _rdpDialog(String id) async {
|
|||||||
hintText: '3389'),
|
hintText: '3389'),
|
||||||
controller: portController,
|
controller: portController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||||
Row(
|
Obx(() => Row(
|
||||||
children: [
|
children: [
|
||||||
(isDesktop || isWebDesktop)
|
stateGlobal.isPortrait.isFalse
|
||||||
? ConstrainedBox(
|
? ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${translate('Username')}:",
|
"${translate('Username')}:",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
).marginOnly(right: 10))
|
).marginOnly(right: 10))
|
||||||
: SizedBox.shrink(),
|
: SizedBox.shrink(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
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,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: (isDesktop || isWebDesktop)
|
labelText:
|
||||||
? null
|
isDesktop ? null : translate('Username')),
|
||||||
: translate('Password'),
|
controller: userController,
|
||||||
suffixIcon: IconButton(
|
).workaroundFreezeLinuxMint(),
|
||||||
onPressed: () => secure.value = !secure.value,
|
),
|
||||||
icon: Icon(secure.value
|
],
|
||||||
? Icons.visibility_off
|
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||||
: Icons.visibility))),
|
Obx(() => Row(
|
||||||
controller: passwordController,
|
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,
|
||||||
|
).workaroundFreezeLinuxMint()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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_model.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/models/peer_tab_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:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -76,8 +77,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
|
|
||||||
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||||
|
|
||||||
@override
|
_PeerTabPageState() {
|
||||||
void initState() {
|
_loadLocalOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadLocalOptions() {
|
||||||
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||||
if (uiType != '') {
|
if (uiType != '') {
|
||||||
peerCardUiType.value = int.parse(uiType) == 0
|
peerCardUiType.value = int.parse(uiType) == 0
|
||||||
@@ -87,8 +91,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
: PeerUiType.list;
|
: PeerUiType.list;
|
||||||
}
|
}
|
||||||
hideAbTagsPanel.value =
|
hideAbTagsPanel.value =
|
||||||
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
|
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
|
||||||
super.initState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleTabSelection(int tabIndex) async {
|
Future<void> handleTabSelection(int tabIndex) async {
|
||||||
@@ -105,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
final model = Provider.of<PeerTabModel>(context);
|
||||||
Widget selectionWrap(Widget widget) {
|
Widget selectionWrap(Widget widget) {
|
||||||
return model.multiSelectionMode ? createMultiSelectionBar() : widget;
|
return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
textBaseline: TextBaseline.ideographic,
|
textBaseline: TextBaseline.ideographic,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
Obx(() => SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: (isDesktop || isWebDesktop)
|
padding: stateGlobal.isPortrait.isTrue
|
||||||
? null
|
? EdgeInsets.symmetric(horizontal: 2)
|
||||||
: EdgeInsets.symmetric(horizontal: 2),
|
: null,
|
||||||
child: selectionWrap(Row(
|
child: selectionWrap(Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child: visibleContextMenuListener(
|
||||||
visibleContextMenuListener(_createSwitchBar(context))),
|
_createSwitchBar(context))),
|
||||||
if (isMobile)
|
if (stateGlobal.isPortrait.isTrue)
|
||||||
..._mobileRightActions(context)
|
..._portraitRightActions(context)
|
||||||
else
|
else
|
||||||
..._desktopRightActions(context)
|
..._landscapeRightActions(context)
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
|
).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
|
||||||
_createPeersView(),
|
_createPeersView(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -297,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget visibleContextMenuListener(Widget child) {
|
Widget visibleContextMenuListener(Widget child) {
|
||||||
if (isMobile) {
|
if (!(isDesktop || isWebDesktop)) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onLongPressDown: (e) {
|
onLongPressDown: (e) {
|
||||||
final x = e.globalPosition.dx;
|
final x = e.globalPosition.dx;
|
||||||
@@ -359,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget createMultiSelectionBar() {
|
Widget createMultiSelectionBar(PeerTabModel model) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -378,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
selectionCount(model.selectedPeers.length),
|
selectionCount(model.selectedPeers.length),
|
||||||
selectAll(),
|
selectAll(model),
|
||||||
closeSelection(),
|
closeSelection(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -454,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
showToast(translate('Successful'));
|
showToast(translate('Successful'));
|
||||||
},
|
},
|
||||||
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
|
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
|
||||||
).marginOnly(left: isMobile ? 11 : 6),
|
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +477,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
model.setMultiSelectionMode(false);
|
model.setMultiSelectionMode(false);
|
||||||
},
|
},
|
||||||
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
|
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
|
||||||
).marginOnly(left: isMobile ? 11 : 6),
|
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +500,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Icon(Icons.tag))
|
child: Icon(Icons.tag))
|
||||||
.marginOnly(left: isMobile ? 11 : 6),
|
.marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget selectAll() {
|
Widget selectAll(PeerTabModel model) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
|
||||||
return Offstage(
|
return Offstage(
|
||||||
offstage:
|
offstage:
|
||||||
model.selectedPeers.length >= model.currentTabCachedPeers.length,
|
model.selectedPeers.length >= model.currentTabCachedPeers.length,
|
||||||
@@ -554,10 +555,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _desktopRightActions(BuildContext context) {
|
List<Widget> _landscapeRightActions(BuildContext context) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
final model = Provider.of<PeerTabModel>(context);
|
||||||
return [
|
return [
|
||||||
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
|
const PeerSearchBar().marginOnly(right: 13),
|
||||||
_createRefresh(
|
_createRefresh(
|
||||||
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
||||||
_createRefresh(
|
_createRefresh(
|
||||||
@@ -578,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 model = Provider.of<PeerTabModel>(context);
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
|
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
|
||||||
@@ -699,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
|||||||
baseOffset: 0,
|
baseOffset: 0,
|
||||||
extentOffset: peerSearchTextController.value.text.length);
|
extentOffset: peerSearchTextController.value.text.length);
|
||||||
});
|
});
|
||||||
return Container(
|
return Obx(() => Container(
|
||||||
width: isMobile ? 120 : 140,
|
width: stateGlobal.isPortrait.isTrue ? 120 : 140,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.background,
|
color: Theme.of(context).colorScheme.background,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Obx(() => Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -742,7 +743,7 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
|||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
// Icon(Icons.close),
|
// Icon(Icons.close),
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -766,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,16 +873,18 @@ class PeerSortDropdown extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||||
@override
|
_PeerSortDropdownState() {
|
||||||
void initState() {
|
|
||||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||||
peerSort.value = PeerSortType.remoteId;
|
_loadLocalOptions();
|
||||||
bind.setLocalFlutterOption(
|
|
||||||
k: kOptionPeerSorting,
|
|
||||||
v: peerSort.value,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
super.initState();
|
}
|
||||||
|
|
||||||
|
void _loadLocalOptions() {
|
||||||
|
peerSort.value = PeerSortType.remoteId;
|
||||||
|
bind.setLocalFlutterOption(
|
||||||
|
k: kOptionPeerSorting,
|
||||||
|
v: peerSort.value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import 'package:dynamic_layouts/dynamic_layouts.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.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:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
@@ -41,14 +43,26 @@ class LoadEvent {
|
|||||||
static const String group = 'load_group_peers';
|
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
|
/// for peer search text, global obs value
|
||||||
final peerSearchText = "".obs;
|
final peerSearchText = "".obs;
|
||||||
|
|
||||||
/// for peer sort, global obs value
|
/// for peer sort, global obs value
|
||||||
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
RxString? _peerSort;
|
||||||
|
RxString get peerSort {
|
||||||
|
_peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||||
|
return _peerSort!;
|
||||||
|
}
|
||||||
|
|
||||||
// list for listener
|
// list for listener
|
||||||
final obslist = [peerSearchText, peerSort].obs;
|
RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
|
||||||
|
|
||||||
final peerSearchTextController =
|
final peerSearchTextController =
|
||||||
TextEditingController(text: peerSearchText.value);
|
TextEditingController(text: peerSearchText.value);
|
||||||
@@ -70,7 +84,8 @@ class _PeersView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// State for the peer widget.
|
/// 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;
|
static const int _maxQueryCount = 3;
|
||||||
final HashMap<String, String> _emptyMessages = HashMap.from({
|
final HashMap<String, String> _emptyMessages = HashMap.from({
|
||||||
LoadEvent.recent: 'empty_recent_tip',
|
LoadEvent.recent: 'empty_recent_tip',
|
||||||
@@ -83,8 +98,10 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
var _lastChangeTime = DateTime.now();
|
var _lastChangeTime = DateTime.now();
|
||||||
var _lastQueryPeers = <String>{};
|
var _lastQueryPeers = <String>{};
|
||||||
var _lastQueryTime = DateTime.now();
|
var _lastQueryTime = DateTime.now();
|
||||||
|
var _lastWindowRestoreTime = DateTime.now();
|
||||||
var _queryCount = 0;
|
var _queryCount = 0;
|
||||||
var _exit = false;
|
var _exit = false;
|
||||||
|
bool _isActive = true;
|
||||||
|
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
@@ -95,12 +112,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
windowManager.removeListener(this);
|
windowManager.removeListener(this);
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_exit = true;
|
_exit = true;
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -108,17 +127,61 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
@override
|
@override
|
||||||
void onWindowFocus() {
|
void onWindowFocus() {
|
||||||
_queryCount = 0;
|
_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
|
@override
|
||||||
void onWindowMinimize() {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<Peers>(
|
// We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
|
||||||
create: (context) => widget.peers,
|
// 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) {
|
child: Consumer<Peers>(builder: (context, peers, child) {
|
||||||
if (peers.peers.isEmpty) {
|
if (peers.peers.isEmpty) {
|
||||||
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
||||||
@@ -172,7 +235,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
var peers = snapshot.data!;
|
var peers = snapshot.data!;
|
||||||
if (peers.length > 1000) peers = peers.sublist(0, 1000);
|
if (peers.length > 1000) peers = peers.sublist(0, 1000);
|
||||||
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
|
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
|
||||||
buildOnePeer(Peer peer) {
|
buildOnePeer(Peer peer, bool isPortrait) {
|
||||||
final visibilityChild = VisibilityDetector(
|
final visibilityChild = VisibilityDetector(
|
||||||
key: ValueKey(_cardId(peer.id)),
|
key: ValueKey(_cardId(peer.id)),
|
||||||
onVisibilityChanged: onVisibilityChanged,
|
onVisibilityChanged: onVisibilityChanged,
|
||||||
@@ -184,7 +247,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
// No need to listen the currentTab change event.
|
// No need to listen the currentTab change event.
|
||||||
// Because the currentTab change event will trigger the peers change event,
|
// Because the currentTab change event will trigger the peers change event,
|
||||||
// and the peers change event will trigger _buildPeersView().
|
// and the peers change event will trigger _buildPeersView().
|
||||||
return (isDesktop || isWebDesktop)
|
return !isPortrait
|
||||||
? Obx(() => peerCardUiType.value == PeerUiType.list
|
? Obx(() => peerCardUiType.value == PeerUiType.list
|
||||||
? Container(height: 45, child: visibilityChild)
|
? Container(height: 45, child: visibilityChild)
|
||||||
: peerCardUiType.value == PeerUiType.grid
|
: peerCardUiType.value == PeerUiType.grid
|
||||||
@@ -195,44 +258,36 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
: Container(child: visibilityChild);
|
: Container(child: visibilityChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Widget child;
|
// We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
|
||||||
if (isMobile) {
|
// Continious rebuilds of `ListView.builder` will cause memory leak.
|
||||||
child = ListView.builder(
|
// Simple demo can reproduce this issue.
|
||||||
itemCount: peers.length,
|
final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
|
||||||
itemBuilder: (BuildContext context, int index) {
|
? ListView.builder(
|
||||||
return buildOnePeer(peers[index]).marginOnly(
|
itemCount: peers.length,
|
||||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
itemBuilder: (BuildContext context, int index) {
|
||||||
},
|
return buildOnePeer(peers[index], true).marginOnly(
|
||||||
);
|
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||||
} else {
|
},
|
||||||
child = Obx(() => peerCardUiType.value == PeerUiType.list
|
)
|
||||||
? DesktopScrollWrapper(
|
: peerCardUiType.value == PeerUiType.list
|
||||||
scrollController: _scrollController,
|
? ListView.builder(
|
||||||
child: ListView.builder(
|
controller: _scrollController,
|
||||||
controller: _scrollController,
|
itemCount: peers.length,
|
||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
itemBuilder: (BuildContext context, int index) {
|
||||||
itemCount: peers.length,
|
return buildOnePeer(peers[index], false).marginOnly(
|
||||||
itemBuilder: (BuildContext context, int index) {
|
right: space,
|
||||||
return buildOnePeer(peers[index]).marginOnly(
|
top: index == 0 ? 0 : space / 2,
|
||||||
right: space,
|
bottom: space / 2);
|
||||||
top: index == 0 ? 0 : space / 2,
|
},
|
||||||
bottom: space / 2);
|
)
|
||||||
}),
|
: DynamicGridView.builder(
|
||||||
)
|
gridDelegate: SliverGridDelegateWithWrapping(
|
||||||
: DesktopScrollWrapper(
|
mainAxisSpacing: space / 2,
|
||||||
scrollController: _scrollController,
|
crossAxisSpacing: space),
|
||||||
child: DynamicGridView.builder(
|
itemCount: peers.length,
|
||||||
controller: _scrollController,
|
itemBuilder: (BuildContext context, int index) {
|
||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
return buildOnePeer(peers[index], false);
|
||||||
gridDelegate: SliverGridDelegateWithWrapping(
|
}));
|
||||||
mainAxisSpacing: space / 2,
|
|
||||||
crossAxisSpacing: space),
|
|
||||||
itemCount: peers.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
return buildOnePeer(peers[index]);
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateEvent == UpdateEvent.load) {
|
if (updateEvent == UpdateEvent.load) {
|
||||||
_curPeers.clear();
|
_curPeers.clear();
|
||||||
@@ -268,7 +323,12 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
_queryOnlines(false);
|
_queryOnlines(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_queryCount < _maxQueryCount || !p) {
|
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 (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||||
if (_curPeers.isNotEmpty) {
|
if (_curPeers.isNotEmpty) {
|
||||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
@@ -286,14 +346,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
_queryOnlines(bool isLoadEvent) {
|
_queryOnlines(bool isLoadEvent) {
|
||||||
if (_curPeers.isNotEmpty) {
|
if (_curPeers.isNotEmpty) {
|
||||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
_lastQueryPeers = {..._curPeers};
|
|
||||||
if (isLoadEvent) {
|
|
||||||
_lastChangeTime = DateTime.now();
|
|
||||||
} else {
|
|
||||||
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
|
||||||
}
|
|
||||||
_queryCount = 0;
|
_queryCount = 0;
|
||||||
}
|
}
|
||||||
|
_lastQueryPeers = {..._curPeers};
|
||||||
|
if (isLoadEvent) {
|
||||||
|
_lastChangeTime = DateTime.now();
|
||||||
|
} else {
|
||||||
|
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Peer>>? matchPeers(
|
Future<List<Peer>>? matchPeers(
|
||||||
@@ -349,28 +409,39 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class BasePeersView extends StatelessWidget {
|
abstract class BasePeersView extends StatelessWidget {
|
||||||
final String name;
|
final PeerTabIndex peerTabIndex;
|
||||||
final String loadEvent;
|
|
||||||
final PeerFilter? peerFilter;
|
final PeerFilter? peerFilter;
|
||||||
final PeerCardBuilder peerCardBuilder;
|
final PeerCardBuilder peerCardBuilder;
|
||||||
final GetInitPeers? getInitPeers;
|
|
||||||
|
|
||||||
const BasePeersView({
|
const BasePeersView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.name,
|
required this.peerTabIndex,
|
||||||
required this.loadEvent,
|
|
||||||
this.peerFilter,
|
this.peerFilter,
|
||||||
required this.peerCardBuilder,
|
required this.peerCardBuilder,
|
||||||
required this.getInitPeers,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return _PeersView(
|
||||||
peers:
|
peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
|
||||||
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
|
|
||||||
peerFilter: peerFilter,
|
|
||||||
peerCardBuilder: peerCardBuilder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,13 +450,11 @@ class RecentPeersView extends BasePeersView {
|
|||||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'recent peer',
|
peerTabIndex: PeerTabIndex.recent,
|
||||||
loadEvent: LoadEvent.recent,
|
|
||||||
peerCardBuilder: (Peer peer) => RecentPeerCard(
|
peerCardBuilder: (Peer peer) => RecentPeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -401,13 +470,11 @@ class FavoritePeersView extends BasePeersView {
|
|||||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'favorite peer',
|
peerTabIndex: PeerTabIndex.fav,
|
||||||
loadEvent: LoadEvent.favorite,
|
|
||||||
peerCardBuilder: (Peer peer) => FavoritePeerCard(
|
peerCardBuilder: (Peer peer) => FavoritePeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -423,13 +490,11 @@ class DiscoveredPeersView extends BasePeersView {
|
|||||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'discovered peer',
|
peerTabIndex: PeerTabIndex.lan,
|
||||||
loadEvent: LoadEvent.lan,
|
|
||||||
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
|
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -442,36 +507,38 @@ class DiscoveredPeersView extends BasePeersView {
|
|||||||
|
|
||||||
class AddressBookPeersView extends BasePeersView {
|
class AddressBookPeersView extends BasePeersView {
|
||||||
AddressBookPeersView(
|
AddressBookPeersView(
|
||||||
{Key? key,
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
EdgeInsets? menuPadding,
|
|
||||||
ScrollController? scrollController,
|
|
||||||
required GetInitPeers getInitPeers})
|
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'address book peer',
|
peerTabIndex: PeerTabIndex.ab,
|
||||||
loadEvent: LoadEvent.addressBook,
|
|
||||||
peerFilter: (Peer peer) =>
|
peerFilter: (Peer peer) =>
|
||||||
_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
||||||
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
|
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: getInitPeers,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||||
if (selectedTags.isEmpty) {
|
if (selectedTags.isEmpty) {
|
||||||
return true;
|
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) {
|
if (gFFI.abModel.filterByIntersection.value) {
|
||||||
for (final tag in selectedTags) {
|
for (final tag in selectedNormalTags) {
|
||||||
if (!idents.contains(tag)) {
|
if (!idents.contains(tag)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
for (final tag in selectedTags) {
|
for (final tag in selectedNormalTags) {
|
||||||
if (idents.contains(tag)) {
|
if (idents.contains(tag)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -483,20 +550,15 @@ class AddressBookPeersView extends BasePeersView {
|
|||||||
|
|
||||||
class MyGroupPeerView extends BasePeersView {
|
class MyGroupPeerView extends BasePeersView {
|
||||||
MyGroupPeerView(
|
MyGroupPeerView(
|
||||||
{Key? key,
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
EdgeInsets? menuPadding,
|
|
||||||
ScrollController? scrollController,
|
|
||||||
required GetInitPeers getInitPeers})
|
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'group peer',
|
peerTabIndex: PeerTabIndex.group,
|
||||||
loadEvent: LoadEvent.group,
|
|
||||||
peerFilter: filter,
|
peerFilter: filter,
|
||||||
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
|
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: getInitPeers,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static bool filter(Peer peer) {
|
static bool filter(Peer peer) {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return FocusScope(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: Focus(
|
child: Focus(
|
||||||
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
|
|||||||
canRequestFocus: true,
|
canRequestFocus: true,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
onFocusChange: onFocusChange,
|
onFocusChange: onFocusChange,
|
||||||
onKey: (FocusNode data, RawKeyEvent e) =>
|
onKey: useRawKeyEvents
|
||||||
inputModel.handleRawKeyEvent(e),
|
? (FocusNode data, RawKeyEvent event) =>
|
||||||
|
inputModel.handleRawKeyEvent(event)
|
||||||
|
: null,
|
||||||
|
onKeyEvent: useRawKeyEvents
|
||||||
|
? null
|
||||||
|
: (FocusNode node, KeyEvent event) =>
|
||||||
|
inputModel.handleKeyEvent(event),
|
||||||
child: child));
|
child: child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,8 +84,17 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||||
double _scale = 1;
|
double _scale = 1;
|
||||||
|
|
||||||
|
// Workaround tap down event when two fingers are used to scale(mobile)
|
||||||
|
TapDownDetails? _lastTapDownDetails;
|
||||||
|
|
||||||
PointerDeviceKind? lastDeviceKind;
|
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;
|
FFI get ffi => widget.ffi;
|
||||||
FfiModel get ffiModel => widget.ffiModel;
|
FfiModel get ffiModel => widget.ffiModel;
|
||||||
InputModel get inputModel => widget.inputModel;
|
InputModel get inputModel => widget.inputModel;
|
||||||
@@ -90,152 +109,190 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTapDown(TapDownDetails d) {
|
onTapDown(TapDownDetails d) async {
|
||||||
lastDeviceKind = d.kind;
|
lastDeviceKind = d.kind;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
|
_lastPosOfDoubleTapDown = d.localPosition;
|
||||||
// Desktop or mobile "Touch mode"
|
// Desktop or mobile "Touch mode"
|
||||||
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
_lastTapDownDetails = d;
|
||||||
inputModel.tapDown(MouseButtons.left);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTapUp(TapUpDetails d) {
|
onTapUp(TapUpDetails d) async {
|
||||||
|
final TapDownDetails? lastTapDownDetails = _lastTapDownDetails;
|
||||||
|
_lastTapDownDetails = null;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
final isMoved =
|
||||||
inputModel.tapUp(MouseButtons.left);
|
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) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!handleTouch) {
|
if (!handleTouch) {
|
||||||
// Mobile, "Mouse mode"
|
// Mobile, "Mouse mode"
|
||||||
inputModel.tap(MouseButtons.left);
|
await inputModel.tap(MouseButtons.left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubleTapDown(TapDownDetails d) {
|
onDoubleTapDown(TapDownDetails d) async {
|
||||||
lastDeviceKind = d.kind;
|
lastDeviceKind = d.kind;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
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) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
inputModel.tap(MouseButtons.left);
|
if (handleTouch &&
|
||||||
inputModel.tap(MouseButtons.left);
|
!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;
|
lastDeviceKind = d.kind;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
_lastPosOfDoubleTapDown = d.localPosition;
|
||||||
_cacheLongPressPosition = d.localPosition;
|
_cacheLongPressPosition = d.localPosition;
|
||||||
|
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
|
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLongPressUp() {
|
onLongPressUp() async {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
inputModel.tapUp(MouseButtons.left);
|
await inputModel.tapUp(MouseButtons.left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for mobiles
|
// for mobiles
|
||||||
onLongPress() {
|
onLongPress() async {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
ffi.cursorModel
|
final isMoved = await ffi.cursorModel
|
||||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
.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;
|
lastDeviceKind = d.kind;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_doubleFinerTapPosition = d.localPosition;
|
||||||
// ignore for desktop and mobile
|
// ignore for desktop and mobile
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubleFinerTap(TapDownDetails d) {
|
onDoubleFinerTap(TapDownDetails d) async {
|
||||||
lastDeviceKind = d.kind;
|
lastDeviceKind = d.kind;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
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;
|
lastDeviceKind = d.kind;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!handleTouch) {
|
if (!handleTouch) {
|
||||||
inputModel.sendMouse('down', MouseButtons.left);
|
await inputModel.sendMouse('down', MouseButtons.left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onHoldDragUpdate(DragUpdateDetails d) {
|
onHoldDragUpdate(DragUpdateDetails d) async {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!handleTouch) {
|
if (!handleTouch) {
|
||||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onHoldDragEnd(DragEndDetails d) {
|
onHoldDragEnd(DragEndDetails d) async {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!handleTouch) {
|
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;
|
lastDeviceKind = d.kind ?? lastDeviceKind;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
|
if (lastTapDownDetails != null) {
|
||||||
|
await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx,
|
||||||
|
lastTapDownDetails.localPosition.dy);
|
||||||
|
}
|
||||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isDesktop) {
|
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_touchModePanStarted = true;
|
||||||
|
if (isDesktop || isWebDesktop) {
|
||||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
// 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,
|
// 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.
|
// we consider to use the long press position as the start position.
|
||||||
@@ -243,11 +300,11 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
// TODO: We should find a better way to send the first pan event as soon as possible.
|
// TODO: We should find a better way to send the first pan event as soon as possible.
|
||||||
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
|
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
|
||||||
500) {
|
500) {
|
||||||
ffi.cursorModel
|
await ffi.cursorModel
|
||||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||||
}
|
}
|
||||||
inputModel.sendMouse('down', MouseButtons.left);
|
await inputModel.sendMouse('down', MouseButtons.left);
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
} else {
|
} else {
|
||||||
final offset = ffi.cursorModel.offset;
|
final offset = ffi.cursorModel.offset;
|
||||||
final cursorX = offset.dx;
|
final cursorX = offset.dx;
|
||||||
@@ -256,39 +313,46 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
|
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
|
||||||
final size = MediaQueryData.fromView(View.of(context)).size;
|
final size = MediaQueryData.fromView(View.of(context)).size;
|
||||||
if (!visible.contains(Offset(cursorX, cursorY))) {
|
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) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
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) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
ffi.cursorModel.clearRemoteWindowCoords();
|
ffi.cursorModel.clearRemoteWindowCoords();
|
||||||
}
|
}
|
||||||
inputModel.sendMouse('up', MouseButtons.left);
|
if (handleTouch) {
|
||||||
|
await inputModel.sendMouse('up', MouseButtons.left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scale + pan event
|
// scale + pan event
|
||||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||||
|
_lastTapDownDetails = null;
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
|
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -297,7 +361,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
_scale = d.scale;
|
_scale = d.scale;
|
||||||
|
|
||||||
if (scale != 0) {
|
if (scale != 0) {
|
||||||
bind.sessionSendPointer(
|
await bind.sessionSendPointer(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
msg: json.encode(
|
msg: json.encode(
|
||||||
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
|
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
|
||||||
@@ -312,21 +376,22 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTwoFingerScaleEnd(ScaleEndDetails d) {
|
onTwoFingerScaleEnd(ScaleEndDetails d) async {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((isDesktop || isWebDesktop)) {
|
if ((isDesktop || isWebDesktop)) {
|
||||||
bind.sessionSendPointer(
|
await bind.sessionSendPointer(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
msg: json.encode(
|
msg: json.encode(
|
||||||
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
|
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
|
||||||
} else {
|
} else {
|
||||||
// mobile
|
// mobile
|
||||||
_scale = 1;
|
_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;
|
get onHoldDragCancel => null;
|
||||||
|
|||||||
@@ -130,12 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// paste
|
// paste
|
||||||
if (isMobile &&
|
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
|
||||||
pi.platform != kPeerPlatformAndroid &&
|
|
||||||
perms['keyboard'] != false &&
|
|
||||||
perms['clipboard'] != false) {
|
|
||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
child: Text(translate('Paste')),
|
child: Text(translate('Send clipboard keystrokes')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
if (data != null && data.text != null) {
|
if (data != null && data.text != null) {
|
||||||
@@ -150,12 +147,23 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
child: Text(translate('Reset canvas')),
|
child: Text(translate('Reset canvas')),
|
||||||
onPressed: () => ffi.cursorModel.reset()));
|
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
|
// transferFile
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Text(translate('Transfer file')),
|
child: Text(translate('Transfer file')),
|
||||||
onPressed: () => connect(context, id, isFileTransfer: true)),
|
onPressed: () =>
|
||||||
|
connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// tcpTunneling
|
// tcpTunneling
|
||||||
@@ -163,7 +171,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Text(translate('TCP tunneling')),
|
child: Text(translate('TCP tunneling')),
|
||||||
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
onPressed: () =>
|
||||||
|
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// note
|
// note
|
||||||
@@ -186,7 +195,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Text('${translate("Insert")} Ctrl + Alt + Del'),
|
child: Text('${translate("Insert Ctrl + Alt + Del")}'),
|
||||||
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
|
|||||||
const String kPeerPlatformLinux = "Linux";
|
const String kPeerPlatformLinux = "Linux";
|
||||||
const String kPeerPlatformMacOS = "Mac OS";
|
const String kPeerPlatformMacOS = "Mac OS";
|
||||||
const String kPeerPlatformAndroid = "Android";
|
const String kPeerPlatformAndroid = "Android";
|
||||||
|
const String kPeerPlatformWebDesktop = "WebDesktop";
|
||||||
|
|
||||||
const double kScrollbarThickness = 12.0;
|
const double kScrollbarThickness = 12.0;
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
|
|||||||
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
||||||
const String kOptionEnableHwcodec = "enable-hwcodec";
|
const String kOptionEnableHwcodec = "enable-hwcodec";
|
||||||
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
||||||
|
const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing";
|
||||||
const String kOptionVideoSaveDirectory = "video-save-directory";
|
const String kOptionVideoSaveDirectory = "video-save-directory";
|
||||||
const String kOptionAccessMode = "access-mode";
|
const String kOptionAccessMode = "access-mode";
|
||||||
const String kOptionEnableKeyboard = "enable-keyboard";
|
const String kOptionEnableKeyboard = "enable-keyboard";
|
||||||
@@ -136,6 +138,7 @@ const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
|||||||
const String kOptionStopService = "stop-service";
|
const String kOptionStopService = "stop-service";
|
||||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||||
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||||
|
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||||
|
|
||||||
// buildin opitons
|
// buildin opitons
|
||||||
const String kOptionHideServerSetting = "hide-server-settings";
|
const String kOptionHideServerSetting = "hide-server-settings";
|
||||||
@@ -166,6 +169,13 @@ const int kWindowMainId = 0;
|
|||||||
const String kPointerEventKindTouch = "touch";
|
const String kPointerEventKindTouch = "touch";
|
||||||
const String kPointerEventKindMouse = "mouse";
|
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 kKeyFlutterKey = "flutter_key";
|
||||||
|
|
||||||
const String kKeyShowDisplaysAsIndividualWindows =
|
const String kKeyShowDisplaysAsIndividualWindows =
|
||||||
@@ -234,15 +244,11 @@ const double kDesktopIconButtonSplashRadius = 20;
|
|||||||
/// [kMinCursorSize] indicates min cursor (w, h)
|
/// [kMinCursorSize] indicates min cursor (w, h)
|
||||||
const int kMinCursorSize = 12;
|
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 kFullScreenEdgeSize = 0.0;
|
||||||
const kMaximizeEdgeSize = 0.0;
|
const kMaximizeEdgeSize = 0.0;
|
||||||
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
|
||||||
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
const kWindowResizeEdgeSize = 5.0;
|
||||||
final kWindowBorderWidth = 1.0;
|
const kWindowBorderWidth = 1.0;
|
||||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||||
const kFrameBorderRadius = 12.0;
|
const kFrameBorderRadius = 12.0;
|
||||||
const kFrameClipRRectBorderRadius = 12.0;
|
const kFrameClipRRectBorderRadius = 12.0;
|
||||||
@@ -568,3 +574,5 @@ enum WindowsTarget {
|
|||||||
extension WindowsTargetExt on int {
|
extension WindowsTargetExt on int {
|
||||||
WindowsTarget get windowsVersion => getWindowsTarget(this);
|
WindowsTarget get windowsVersion => getWindowsTarget(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:auto_size_text/auto_size_text.dart';
|
|
||||||
import 'package:flutter/material.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/consts.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -39,7 +39,7 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
|
|||||||
double? get height => bind.isIncomingOnly() ? null : em * 3;
|
double? get height => bind.isIncomingOnly() ? null : em * 3;
|
||||||
|
|
||||||
void onUsePublicServerGuide() {
|
void onUsePublicServerGuide() {
|
||||||
const url = "https://rustdesk.com/pricing.html";
|
const url = "https://rustdesk.com/pricing";
|
||||||
canLaunchUrlString(url).then((can) {
|
canLaunchUrlString(url).then((can) {
|
||||||
if (can) {
|
if (can) {
|
||||||
launchUrlString(url);
|
launchUrlString(url);
|
||||||
@@ -169,20 +169,19 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
|
|||||||
final status =
|
final status =
|
||||||
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
||||||
final statusNum = status['status_num'] as int;
|
final statusNum = status['status_num'] as int;
|
||||||
final preStatus = stateGlobal.svcStatus.value;
|
|
||||||
if (statusNum == 0) {
|
if (statusNum == 0) {
|
||||||
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
||||||
} else if (statusNum == -1) {
|
} else if (statusNum == -1) {
|
||||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||||
} else if (statusNum == 1) {
|
} else if (statusNum == 1) {
|
||||||
stateGlobal.svcStatus.value = SvcStatus.ready;
|
stateGlobal.svcStatus.value = SvcStatus.ready;
|
||||||
if (preStatus != SvcStatus.ready) {
|
|
||||||
gFFI.userModel.refreshCurrentUser();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||||
}
|
}
|
||||||
_svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
|
_svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
|
||||||
|
try {
|
||||||
|
stateGlobal.videoConnCount.value = status['video_conn_count'] as int;
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,19 +206,21 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
|
|
||||||
bool isPeersLoading = false;
|
bool isPeersLoading = false;
|
||||||
bool isPeersLoaded = false;
|
bool isPeersLoaded = false;
|
||||||
|
// https://github.com/flutter/flutter/issues/157244
|
||||||
|
Iterable<Peer> _autocompleteOpts = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (_idController.text.isEmpty) {
|
if (_idController.text.isEmpty) {
|
||||||
() async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||||
if (lastRemoteId != _idController.id) {
|
if (lastRemoteId != _idController.id) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_idController.id = lastRemoteId;
|
_idController.id = lastRemoteId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}();
|
});
|
||||||
}
|
}
|
||||||
Get.put<IDTextEditingController>(_idController);
|
Get.put<IDTextEditingController>(_idController);
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
@@ -261,8 +262,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
@override
|
@override
|
||||||
void onWindowLeaveFullScreen() {
|
void onWindowLeaveFullScreen() {
|
||||||
// Restore edge border to default edge size.
|
// Restore edge border to default edge size.
|
||||||
stateGlobal.resizeEdgeSize.value =
|
stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
|
||||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
|
? kMaximizeEdgeSize
|
||||||
|
: windowResizeEdgeSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -326,43 +328,14 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
child: Ink(
|
child: Ink(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
getConnectionPageTitle(context, false).marginOnly(bottom: 15),
|
||||||
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("id_input_tip"),
|
|
||||||
child: Icon(
|
|
||||||
Icons.help_outline_outlined,
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleLarge
|
|
||||||
?.color
|
|
||||||
?.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
).marginOnly(bottom: 15),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Autocomplete<Peer>(
|
child: Autocomplete<Peer>(
|
||||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
if (textEditingValue.text == '') {
|
if (textEditingValue.text == '') {
|
||||||
return const Iterable<Peer>.empty();
|
_autocompleteOpts = const Iterable<Peer>.empty();
|
||||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||||
Peer emptyPeer = Peer(
|
Peer emptyPeer = Peer(
|
||||||
id: '',
|
id: '',
|
||||||
@@ -378,7 +351,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
rdpUsername: '',
|
rdpUsername: '',
|
||||||
loginName: '',
|
loginName: '',
|
||||||
);
|
);
|
||||||
return [emptyPeer];
|
_autocompleteOpts = [emptyPeer];
|
||||||
} else {
|
} else {
|
||||||
String textWithoutSpaces =
|
String textWithoutSpaces =
|
||||||
textEditingValue.text.replaceAll(" ", "");
|
textEditingValue.text.replaceAll(" ", "");
|
||||||
@@ -389,8 +362,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
String textToFind = textEditingValue.text.toLowerCase();
|
String textToFind = textEditingValue.text.toLowerCase();
|
||||||
|
_autocompleteOpts = peers
|
||||||
return peers
|
|
||||||
.where((peer) =>
|
.where((peer) =>
|
||||||
peer.id.toLowerCase().contains(textToFind) ||
|
peer.id.toLowerCase().contains(textToFind) ||
|
||||||
peer.username
|
peer.username
|
||||||
@@ -402,6 +374,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
peer.alias.toLowerCase().contains(textToFind))
|
peer.alias.toLowerCase().contains(textToFind))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
return _autocompleteOpts;
|
||||||
},
|
},
|
||||||
fieldViewBuilder: (
|
fieldViewBuilder: (
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -451,7 +424,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
onConnect();
|
onConnect();
|
||||||
},
|
},
|
||||||
));
|
).workaroundFreezeLinuxMint());
|
||||||
},
|
},
|
||||||
onSelected: (option) {
|
onSelected: (option) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -462,6 +435,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
optionsViewBuilder: (BuildContext context,
|
optionsViewBuilder: (BuildContext context,
|
||||||
AutocompleteOnSelected<Peer> onSelected,
|
AutocompleteOnSelected<Peer> onSelected,
|
||||||
Iterable<Peer> options) {
|
Iterable<Peer> options) {
|
||||||
|
options = _autocompleteOpts;
|
||||||
double maxHeight = options.length * 50;
|
double maxHeight = options.length * 50;
|
||||||
if (options.length == 1) {
|
if (options.length == 1) {
|
||||||
maxHeight = 52;
|
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/connection_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_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/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/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_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/plugin/ui_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -35,12 +35,11 @@ class DesktopHomePage extends StatefulWidget {
|
|||||||
const borderColor = Color(0xFF2F65BA);
|
const borderColor = Color(0xFF2F65BA);
|
||||||
|
|
||||||
class _DesktopHomePageState extends State<DesktopHomePage>
|
class _DesktopHomePageState extends State<DesktopHomePage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||||
final _leftPaneScrollController = ScrollController();
|
final _leftPaneScrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
var updateUrl = '';
|
|
||||||
var systemError = '';
|
var systemError = '';
|
||||||
StreamSubscription? _uniLinksSubscription;
|
StreamSubscription? _uniLinksSubscription;
|
||||||
var svcStopped = false.obs;
|
var svcStopped = false.obs;
|
||||||
@@ -52,6 +51,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
bool isCardClosed = false;
|
bool isCardClosed = false;
|
||||||
|
|
||||||
final RxBool _editHover = false.obs;
|
final RxBool _editHover = false.obs;
|
||||||
|
final RxBool _block = false.obs;
|
||||||
|
|
||||||
final GlobalKey _childKey = GlobalKey();
|
final GlobalKey _childKey = GlobalKey();
|
||||||
|
|
||||||
@@ -59,14 +59,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
final isIncomingOnly = bind.isIncomingOnly();
|
final isIncomingOnly = bind.isIncomingOnly();
|
||||||
return Row(
|
return _buildBlock(
|
||||||
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
buildLeftPane(context),
|
buildLeftPane(context),
|
||||||
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
||||||
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBlock({required Widget child}) {
|
||||||
|
return buildRemoteBlock(
|
||||||
|
block: _block, mask: true, use: canBeBlocked, child: child);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildLeftPane(BuildContext context) {
|
Widget buildLeftPane(BuildContext context) {
|
||||||
@@ -87,7 +93,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
if (!isOutgoingOnly) buildIDBoard(context),
|
if (!isOutgoingOnly) buildIDBoard(context),
|
||||||
if (!isOutgoingOnly) buildPasswordBoard(context),
|
if (!isOutgoingOnly) buildPasswordBoard(context),
|
||||||
FutureBuilder<Widget>(
|
FutureBuilder<Widget>(
|
||||||
future: buildHelpCards(),
|
future: Future.value(
|
||||||
|
Obx(() => buildHelpCards(stateGlobal.updateUrl.value))),
|
||||||
builder: (_, data) {
|
builder: (_, data) {
|
||||||
if (data.hasData) {
|
if (data.hasData) {
|
||||||
if (isIncomingOnly) {
|
if (isIncomingOnly) {
|
||||||
@@ -125,47 +132,43 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: isIncomingOnly ? 280.0 : 200.0,
|
width: isIncomingOnly ? 280.0 : 200.0,
|
||||||
color: Theme.of(context).colorScheme.background,
|
color: Theme.of(context).colorScheme.background,
|
||||||
child: DesktopScrollWrapper(
|
child: Stack(
|
||||||
scrollController: _leftPaneScrollController,
|
children: [
|
||||||
child: Stack(
|
SingleChildScrollView(
|
||||||
children: [
|
controller: _leftPaneScrollController,
|
||||||
SingleChildScrollView(
|
child: Column(
|
||||||
controller: _leftPaneScrollController,
|
key: _childKey,
|
||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
children: children,
|
||||||
child: Column(
|
|
||||||
key: _childKey,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (isOutgoingOnly)
|
),
|
||||||
Positioned(
|
if (isOutgoingOnly)
|
||||||
bottom: 6,
|
Positioned(
|
||||||
left: 12,
|
bottom: 6,
|
||||||
child: Align(
|
left: 12,
|
||||||
alignment: Alignment.centerLeft,
|
child: Align(
|
||||||
child: InkWell(
|
alignment: Alignment.centerLeft,
|
||||||
child: Obx(
|
child: InkWell(
|
||||||
() => Icon(
|
child: Obx(
|
||||||
Icons.settings,
|
() => Icon(
|
||||||
color: _editHover.value
|
Icons.settings,
|
||||||
? textColor
|
color: _editHover.value
|
||||||
: Colors.grey.withOpacity(0.5),
|
? textColor
|
||||||
size: 22,
|
: Colors.grey.withOpacity(0.5),
|
||||||
),
|
size: 22,
|
||||||
),
|
),
|
||||||
onTap: () => {
|
|
||||||
if (DesktopSettingPage.tabKeys.isNotEmpty)
|
|
||||||
{
|
|
||||||
DesktopSettingPage.switch2page(
|
|
||||||
DesktopSettingPage.tabKeys[0])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onHover: (value) => _editHover.value = value,
|
|
||||||
),
|
),
|
||||||
|
onTap: () => {
|
||||||
|
if (DesktopSettingPage.tabKeys.isNotEmpty)
|
||||||
|
{
|
||||||
|
DesktopSettingPage.switch2page(
|
||||||
|
DesktopSettingPage.tabKeys[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHover: (value) => _editHover.value = value,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -234,7 +237,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
),
|
),
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -272,10 +275,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildPasswordBoard(BuildContext context) {
|
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 refreshHover = false.obs;
|
||||||
RxBool editHover = false.obs;
|
RxBool editHover = false.obs;
|
||||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||||
|
final showOneTime = model.approveMode != 'click' &&
|
||||||
|
model.verificationMethod != kUsePermanentPassword;
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
|
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -304,8 +318,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onDoubleTap: () {
|
onDoubleTap: () {
|
||||||
if (model.verificationMethod !=
|
if (showOneTime) {
|
||||||
kUsePermanentPassword) {
|
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(text: model.serverPasswd.text));
|
ClipboardData(text: model.serverPasswd.text));
|
||||||
showToast(translate("Copied"));
|
showToast(translate("Copied"));
|
||||||
@@ -320,25 +333,26 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
EdgeInsets.only(top: 14, bottom: 10),
|
EdgeInsets.only(top: 14, bottom: 10),
|
||||||
),
|
),
|
||||||
style: TextStyle(fontSize: 15),
|
style: TextStyle(fontSize: 15),
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
AnimatedRotationWidget(
|
if (showOneTime)
|
||||||
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
AnimatedRotationWidget(
|
||||||
child: Tooltip(
|
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
||||||
message: translate('Refresh Password'),
|
child: Tooltip(
|
||||||
child: Obx(() => RotatedBox(
|
message: translate('Refresh Password'),
|
||||||
quarterTurns: 2,
|
child: Obx(() => RotatedBox(
|
||||||
child: Icon(
|
quarterTurns: 2,
|
||||||
Icons.refresh,
|
child: Icon(
|
||||||
color: refreshHover.value
|
Icons.refresh,
|
||||||
? textColor
|
color: refreshHover.value
|
||||||
: Color(0xFFDDDDDD),
|
? textColor
|
||||||
size: 22,
|
: 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())
|
if (!bind.isDisableSettings())
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
@@ -409,14 +423,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Widget> buildHelpCards() async {
|
Widget buildHelpCards(String updateUrl) {
|
||||||
if (!bind.isCustomClient() &&
|
if (!bind.isCustomClient() &&
|
||||||
updateUrl.isNotEmpty &&
|
updateUrl.isNotEmpty &&
|
||||||
!isCardClosed &&
|
!isCardClosed &&
|
||||||
bind.mainUriPrefixSync().contains('rustdesk')) {
|
bind.mainUriPrefixSync().contains('rustdesk')) {
|
||||||
return buildInstallCard(
|
return buildInstallCard(
|
||||||
"Status",
|
"Status",
|
||||||
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
|
"${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).",
|
||||||
"Click to download", () async {
|
"Click to download", () async {
|
||||||
final Uri url = Uri.parse('https://rustdesk.com/download');
|
final Uri url = Uri.parse('https://rustdesk.com/download');
|
||||||
await launchUrl(url);
|
await launchUrl(url);
|
||||||
@@ -663,12 +677,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (!bind.isCustomClient()) {
|
|
||||||
Timer(const Duration(seconds: 1), () async {
|
|
||||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
|
||||||
if (updateUrl.isNotEmpty) setState(() {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||||
await gFFI.serverModel.fetchID();
|
await gFFI.serverModel.fetchID();
|
||||||
final error = await bind.mainGetError();
|
final error = await bind.mainGetError();
|
||||||
@@ -766,6 +774,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
isRDP: call.arguments['isRDP'],
|
isRDP: call.arguments['isRDP'],
|
||||||
password: call.arguments['password'],
|
password: call.arguments['password'],
|
||||||
forceRelay: call.arguments['forceRelay'],
|
forceRelay: call.arguments['forceRelay'],
|
||||||
|
connToken: call.arguments['connToken'],
|
||||||
);
|
);
|
||||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||||
final args = call.arguments.split(',');
|
final args = call.arguments.split(',');
|
||||||
@@ -803,6 +812,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
_updateWindowSize();
|
_updateWindowSize();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateWindowSize() {
|
_updateWindowSize() {
|
||||||
@@ -824,9 +834,18 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
_uniLinksSubscription?.cancel();
|
_uniLinksSubscription?.cancel();
|
||||||
Get.delete<RxBool>(tag: 'stop-service');
|
Get.delete<RxBool>(tag: 'stop-service');
|
||||||
_updateTimer?.cancel();
|
_updateTimer?.cancel();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildPluginEntry() {
|
Widget buildPluginEntry() {
|
||||||
final entries = PluginUiManager.instance.entries.entries;
|
final entries = PluginUiManager.instance.entries.entries;
|
||||||
return Offstage(
|
return Offstage(
|
||||||
@@ -857,6 +876,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
// SpecialCharacterValidationRule(),
|
// SpecialCharacterValidationRule(),
|
||||||
MinCharactersValidationRule(8),
|
MinCharactersValidationRule(8),
|
||||||
];
|
];
|
||||||
|
final maxLength = bind.mainMaxEncryptLen();
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
submit() {
|
submit() {
|
||||||
@@ -915,7 +935,8 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
errMsg0 = '';
|
errMsg0 = '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
maxLength: maxLength,
|
||||||
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -941,7 +962,8 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
errMsg1 = '';
|
errMsg1 = '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
maxLength: maxLength,
|
||||||
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -37,28 +37,12 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopTabPageState extends State<DesktopTabPage>
|
class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||||
with WidgetsBindingObserver {
|
|
||||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||||
|
|
||||||
final RxBool _block = false.obs;
|
_DesktopTabPageState() {
|
||||||
// bool mouseIn = false;
|
|
||||||
|
|
||||||
@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);
|
|
||||||
Get.put<DesktopTabController>(tabController);
|
|
||||||
RemoteCountState.init();
|
RemoteCountState.init();
|
||||||
|
Get.put<DesktopTabController>(tabController);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: kTabLabelHomePage,
|
key: kTabLabelHomePage,
|
||||||
label: kTabLabelHomePage,
|
label: kTabLabelHomePage,
|
||||||
@@ -81,6 +65,12 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
bool _handleKeyEvent(KeyEvent event) {
|
bool _handleKeyEvent(KeyEvent event) {
|
||||||
if (!mouseIn && event is KeyDownEvent) {
|
if (!mouseIn && event is KeyDownEvent) {
|
||||||
@@ -94,7 +84,6 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
Get.delete<DesktopTabController>();
|
Get.delete<DesktopTabController>();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -116,13 +105,13 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
isClose: false,
|
isClose: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
blockTab: _block,
|
|
||||||
)));
|
)));
|
||||||
return isMacOS || kUseCompatibleUiMode
|
return isMacOS || kUseCompatibleUiMode
|
||||||
? tabWidget
|
? tabWidget
|
||||||
: Obx(
|
: Obx(
|
||||||
() => DragToResizeArea(
|
() => DragToResizeArea(
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||||
child: tabWidget,
|
child: tabWidget,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:extended_text/extended_text.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||||
import 'package:percent_indicator/percent_indicator.dart';
|
import 'package:percent_indicator/percent_indicator.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.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:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.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 '../../consts.dart';
|
||||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||||
@@ -54,21 +57,23 @@ class FileManagerPage extends StatefulWidget {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.password,
|
required this.password,
|
||||||
required this.isSharedPassword,
|
required this.isSharedPassword,
|
||||||
required this.tabController,
|
this.tabController,
|
||||||
|
this.connToken,
|
||||||
this.forceRelay})
|
this.forceRelay})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
final String id;
|
final String id;
|
||||||
final String? password;
|
final String? password;
|
||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
final bool? forceRelay;
|
final bool? forceRelay;
|
||||||
final DesktopTabController tabController;
|
final String? connToken;
|
||||||
|
final DesktopTabController? tabController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FileManagerPageState extends State<FileManagerPage>
|
class _FileManagerPageState extends State<FileManagerPage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||||
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
||||||
|
|
||||||
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
||||||
@@ -87,6 +92,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
isFileTransfer: true,
|
isFileTransfer: true,
|
||||||
password: widget.password,
|
password: widget.password,
|
||||||
isSharedPassword: widget.isSharedPassword,
|
isSharedPassword: widget.isSharedPassword,
|
||||||
|
connToken: widget.connToken,
|
||||||
forceRelay: widget.forceRelay);
|
forceRelay: widget.forceRelay);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_ffi.dialogManager
|
_ffi.dialogManager
|
||||||
@@ -96,9 +102,16 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
if (!isLinux) {
|
if (!isLinux) {
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
}
|
}
|
||||||
|
if (isWeb) {
|
||||||
|
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||||
|
}
|
||||||
debugPrint("File manager page init success with id ${widget.id}");
|
debugPrint("File manager page init success with id ${widget.id}");
|
||||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
_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
|
@override
|
||||||
@@ -111,12 +124,21 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
}
|
}
|
||||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||||
});
|
});
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
jobController.jobTable.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
@@ -126,10 +148,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
if (!isWeb)
|
||||||
flex: 3,
|
Flexible(
|
||||||
child: dropArea(FileManagerView(
|
flex: 3,
|
||||||
model.localController, _ffi, _mouseFocusScope))),
|
child: dropArea(FileManagerView(
|
||||||
|
model.localController, _ffi, _mouseFocusScope))),
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: dropArea(FileManagerView(
|
child: dropArea(FileManagerView(
|
||||||
@@ -170,10 +193,31 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
/// transfer status list
|
/// transfer status list
|
||||||
/// watch transfer status
|
/// watch transfer status
|
||||||
Widget statusList() {
|
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(
|
statusListView(List<JobProgress> jobs) => ListView.builder(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final item = jobs[index];
|
final item = jobs[index];
|
||||||
|
final status = item.getStatus();
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 5),
|
padding: const EdgeInsets.only(bottom: 5),
|
||||||
child: generateCard(
|
child: generateCard(
|
||||||
@@ -183,15 +227,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Transform.rotate(
|
getIcon(item)
|
||||||
angle: item.isRemoteToLocal ? pi : 0,
|
.marginSymmetric(horizontal: 10, vertical: 12),
|
||||||
child: SvgPicture.asset("assets/arrow.svg",
|
|
||||||
colorFilter: svgColor(
|
|
||||||
Theme.of(context).tabBarTheme.labelColor)),
|
|
||||||
).paddingOnly(left: 15),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -200,45 +237,28 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Tooltip(
|
Tooltip(
|
||||||
waitDuration: Duration(milliseconds: 500),
|
waitDuration: Duration(milliseconds: 500),
|
||||||
message: item.jobName,
|
message: item.jobName,
|
||||||
child: Text(
|
child: ExtendedText(
|
||||||
item.fileName,
|
item.jobName,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
).paddingSymmetric(vertical: 10),
|
overflowWidget: TextOverflowWidget(
|
||||||
),
|
child: Text("..."),
|
||||||
Text(
|
position: TextOverflowPosition.start),
|
||||||
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: MyTheme.darkGray,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Offstage(
|
Tooltip(
|
||||||
offstage: item.state != JobState.inProgress,
|
waitDuration: Duration(milliseconds: 500),
|
||||||
child: Text(
|
message: status,
|
||||||
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
|
child: Text(status,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: MyTheme.darkGray,
|
color: MyTheme.darkGray,
|
||||||
),
|
)).marginOnly(top: 6),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: item.state == JobState.inProgress,
|
offstage: item.type != JobType.transfer ||
|
||||||
child: Text(
|
item.state != JobState.inProgress,
|
||||||
translate(
|
|
||||||
item.display(),
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: MyTheme.darkGray,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Offstage(
|
|
||||||
offstage: item.state != JobState.inProgress,
|
|
||||||
child: LinearPercentIndicator(
|
child: LinearPercentIndicator(
|
||||||
padding: EdgeInsets.only(right: 15),
|
|
||||||
animateFromLastPercent: true,
|
animateFromLastPercent: true,
|
||||||
center: Text(
|
center: Text(
|
||||||
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
||||||
@@ -248,7 +268,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
progressColor: MyTheme.accent,
|
progressColor: MyTheme.accent,
|
||||||
backgroundColor: Theme.of(context).hoverColor,
|
backgroundColor: Theme.of(context).hoverColor,
|
||||||
lineHeight: kDesktopFileTransferRowHeight,
|
lineHeight: kDesktopFileTransferRowHeight,
|
||||||
).paddingSymmetric(vertical: 15),
|
).paddingSymmetric(vertical: 8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -259,6 +279,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Offstage(
|
Offstage(
|
||||||
offstage: item.state != JobState.paused,
|
offstage: item.state != JobState.paused,
|
||||||
child: MenuButton(
|
child: MenuButton(
|
||||||
|
tooltip: translate("Resume"),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
jobController.resumeJob(item.id);
|
jobController.resumeJob(item.id);
|
||||||
},
|
},
|
||||||
@@ -271,7 +292,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
padding: EdgeInsets.only(right: 15),
|
tooltip: translate("Delete"),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
"assets/close.svg",
|
"assets/close.svg",
|
||||||
colorFilter: svgColor(Colors.white),
|
colorFilter: svgColor(Colors.white),
|
||||||
@@ -284,11 +305,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
hoverColor: MyTheme.accent80,
|
hoverColor: MyTheme.accent80,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
).marginAll(12),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(vertical: 10),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -472,6 +493,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget headTools() {
|
Widget headTools() {
|
||||||
|
var uploadButtonTapPosition = RelativeRect.fill;
|
||||||
|
RxBool isUploadFolder =
|
||||||
|
(bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
|
||||||
return Container(
|
return Container(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -518,6 +542,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Back'),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: 3,
|
right: 3,
|
||||||
),
|
),
|
||||||
@@ -537,6 +562,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Parent directory'),
|
||||||
child: RotatedBox(
|
child: RotatedBox(
|
||||||
quarterTurns: 3,
|
quarterTurns: 3,
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
@@ -601,6 +627,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
switch (_locationStatus.value) {
|
switch (_locationStatus.value) {
|
||||||
case LocationStatus.bread:
|
case LocationStatus.bread:
|
||||||
return MenuButton(
|
return MenuButton(
|
||||||
|
tooltip: translate('Search'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_locationStatus.value = LocationStatus.fileSearchBar;
|
_locationStatus.value = LocationStatus.fileSearchBar;
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
@@ -627,6 +654,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
);
|
);
|
||||||
case LocationStatus.fileSearchBar:
|
case LocationStatus.fileSearchBar:
|
||||||
return MenuButton(
|
return MenuButton(
|
||||||
|
tooltip: translate('Clear'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onSearchText("", isLocal);
|
onSearchText("", isLocal);
|
||||||
_locationStatus.value = LocationStatus.bread;
|
_locationStatus.value = LocationStatus.bread;
|
||||||
@@ -642,6 +670,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Refresh File'),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: 3,
|
left: 3,
|
||||||
),
|
),
|
||||||
@@ -667,6 +696,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Home'),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: 3,
|
right: 3,
|
||||||
),
|
),
|
||||||
@@ -682,11 +712,27 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
hoverColor: Theme.of(context).hoverColor,
|
hoverColor: Theme.of(context).hoverColor,
|
||||||
),
|
),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Create Folder'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final name = TextEditingController();
|
final name = TextEditingController();
|
||||||
|
String? errorText;
|
||||||
_ffi.dialogManager.show((setState, close, context) {
|
_ffi.dialogManager.show((setState, close, context) {
|
||||||
|
name.addListener(() {
|
||||||
|
if (errorText != null) {
|
||||||
|
setState(() {
|
||||||
|
errorText = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
submit() {
|
submit() {
|
||||||
if (name.value.text.isNotEmpty) {
|
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.createDir(PathUtil.join(
|
||||||
controller.directory.value.path,
|
controller.directory.value.path,
|
||||||
name.value.text,
|
name.value.text,
|
||||||
@@ -718,10 +764,11 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
labelText: translate(
|
labelText: translate(
|
||||||
"Please enter the folder name",
|
"Please enter the folder name",
|
||||||
),
|
),
|
||||||
|
errorText: errorText,
|
||||||
),
|
),
|
||||||
controller: name,
|
controller: name,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -751,6 +798,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
hoverColor: Theme.of(context).hoverColor,
|
hoverColor: Theme.of(context).hoverColor,
|
||||||
),
|
),
|
||||||
Obx(() => MenuButton(
|
Obx(() => MenuButton(
|
||||||
|
tooltip: translate('Delete'),
|
||||||
onPressed: SelectedItems.valid(selectedItems.items)
|
onPressed: SelectedItems.valid(selectedItems.items)
|
||||||
? () async {
|
? () async {
|
||||||
await (controller
|
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(
|
Obx(() => ElevatedButton.icon(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||||
@@ -803,19 +911,22 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
: Colors.white,
|
: Colors.white,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: RotatedBox(
|
: isWeb
|
||||||
quarterTurns: 2,
|
? Offstage()
|
||||||
child: SvgPicture.asset(
|
: RotatedBox(
|
||||||
"assets/arrow.svg",
|
quarterTurns: 2,
|
||||||
colorFilter: svgColor(selectedItems.items.isEmpty
|
child: SvgPicture.asset(
|
||||||
? Theme.of(context).brightness ==
|
"assets/arrow.svg",
|
||||||
Brightness.light
|
colorFilter: svgColor(
|
||||||
? MyTheme.grayBg
|
selectedItems.items.isEmpty
|
||||||
: MyTheme.darkGray
|
? Theme.of(context).brightness ==
|
||||||
: Colors.white),
|
Brightness.light
|
||||||
alignment: Alignment.bottomRight,
|
? MyTheme.grayBg
|
||||||
),
|
: MyTheme.darkGray
|
||||||
),
|
: Colors.white),
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
label: isLocal
|
label: isLocal
|
||||||
? SvgPicture.asset(
|
? SvgPicture.asset(
|
||||||
"assets/arrow.svg",
|
"assets/arrow.svg",
|
||||||
@@ -827,7 +938,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
: Colors.white),
|
: Colors.white),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
translate('Receive'),
|
translate(isWeb ? 'Download' : 'Receive'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selectedItems.items.isEmpty
|
color: selectedItems.items.isEmpty
|
||||||
? Theme.of(context).brightness ==
|
? Theme.of(context).brightness ==
|
||||||
@@ -882,6 +993,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||||
},
|
},
|
||||||
child: MenuButton(
|
child: MenuButton(
|
||||||
|
tooltip: translate('More'),
|
||||||
onPressed: () => mod_menu.showMenu(
|
onPressed: () => mod_menu.showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
position: menuPos,
|
position: menuPos,
|
||||||
@@ -913,6 +1025,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
BuildContext context, ScrollController scrollController) {
|
BuildContext context, ScrollController scrollController) {
|
||||||
final fd = controller.directory.value;
|
final fd = controller.directory.value;
|
||||||
final entries = fd.entries;
|
final entries = fd.entries;
|
||||||
|
Rx<Entry?> rightClickEntry = Rx(null);
|
||||||
|
|
||||||
return ListSearchActionListener(
|
return ListSearchActionListener(
|
||||||
node: _keyboardNode,
|
node: _keyboardNode,
|
||||||
@@ -971,16 +1084,70 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
final lastModifiedStr = entry.isDrive
|
final lastModifiedStr = entry.isDrive
|
||||||
? " "
|
? " "
|
||||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
: "${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(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 1),
|
padding: EdgeInsets.symmetric(vertical: 1),
|
||||||
child: Obx(() => Container(
|
child: Obx(() => Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: selectedItems.items.contains(entry)
|
color: selectedItems.items.contains(entry)
|
||||||
? Theme.of(context).hoverColor
|
? MyTheme.button
|
||||||
: Theme.of(context).cardColor,
|
: Theme.of(context).cardColor,
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.all(
|
||||||
Radius.circular(5.0),
|
Radius.circular(5.0),
|
||||||
),
|
),
|
||||||
|
border: rightClickEntry.value == entry
|
||||||
|
? Border.all(
|
||||||
|
color: MyTheme.button,
|
||||||
|
width: 1.0,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
key: ValueKey(entry.name),
|
key: ValueKey(entry.name),
|
||||||
height: kDesktopFileTransferRowHeight,
|
height: kDesktopFileTransferRowHeight,
|
||||||
@@ -1019,22 +1186,19 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(entry.name.nonBreaking,
|
child: Text(entry.name.nonBreaking,
|
||||||
|
style: TextStyle(
|
||||||
|
color: selectedItems.items
|
||||||
|
.contains(entry)
|
||||||
|
? Colors.white
|
||||||
|
: null),
|
||||||
overflow:
|
overflow:
|
||||||
TextOverflow.ellipsis))
|
TextOverflow.ellipsis))
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: onTap,
|
||||||
final items = selectedItems;
|
onSecondaryTap: onSecondaryTap,
|
||||||
// handle double click
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
if (_checkDoubleClick(entry)) {
|
|
||||||
controller.openDirectory(entry.path);
|
|
||||||
items.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_onSelectedChanged(
|
|
||||||
items, filteredEntries, entry, isLocal);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
@@ -1051,11 +1215,17 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: MyTheme.darkGray,
|
color: selectedItems.items
|
||||||
|
.contains(entry)
|
||||||
|
? Colors.white70
|
||||||
|
: MyTheme.darkGray,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
onSecondaryTap: onSecondaryTap,
|
||||||
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
),
|
),
|
||||||
// Divider from header.
|
// Divider from header.
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -1071,9 +1241,16 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
sizeStr,
|
sizeStr,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10, color: MyTheme.darkGray),
|
fontSize: 10,
|
||||||
|
color:
|
||||||
|
selectedItems.items.contains(entry)
|
||||||
|
? Colors.white70
|
||||||
|
: MyTheme.darkGray),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
onSecondaryTap: onSecondaryTap,
|
||||||
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1480,7 +1657,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
onChanged: _locationStatus.value == LocationStatus.fileSearchBar
|
onChanged: _locationStatus.value == LocationStatus.fileSearchBar
|
||||||
? (searchText) => onSearchText(searchText, isLocal)
|
? (searchText) => onSearchText(searchText, isLocal)
|
||||||
: null,
|
: null,
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(windowId())
|
||||||
.setTitle(getWindowNameWithId(id));
|
.setTitle(getWindowNameWithId(id));
|
||||||
};
|
};
|
||||||
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: params['id'],
|
key: params['id'],
|
||||||
label: params['id'],
|
label: params['id'],
|
||||||
@@ -47,6 +48,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
isSharedPassword: params['isSharedPassword'],
|
isSharedPassword: params['isSharedPassword'],
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
forceRelay: params['forceRelay'],
|
forceRelay: params['forceRelay'],
|
||||||
|
connToken: params['connToken'],
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +56,8 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
|
||||||
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
print(
|
debugPrint(
|
||||||
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
||||||
// for simplify, just replace connectionId
|
// for simplify, just replace connectionId
|
||||||
if (call.method == kWindowEventNewFileTransfer) {
|
if (call.method == kWindowEventNewFileTransfer) {
|
||||||
@@ -77,6 +77,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
isSharedPassword: args['isSharedPassword'],
|
isSharedPassword: args['isSharedPassword'],
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
forceRelay: args['forceRelay'],
|
forceRelay: args['forceRelay'],
|
||||||
|
connToken: args['connToken'],
|
||||||
)));
|
)));
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
@@ -97,6 +98,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
controller: tabController,
|
controller: tabController,
|
||||||
onWindowCloseButton: handleWindowCloseButton,
|
onWindowCloseButton: handleWindowCloseButton,
|
||||||
tail: const AddButton(),
|
tail: const AddButton(),
|
||||||
|
selectedBorderColor: MyTheme.accent,
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
));
|
));
|
||||||
final tabWidget = isLinux
|
final tabWidget = isLinux
|
||||||
@@ -111,6 +113,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
: SubWindowDragToResizeArea(
|
: SubWindowDragToResizeArea(
|
||||||
child: tabWidget,
|
child: tabWidget,
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||||
windowId: stateGlobal.windowId,
|
windowId: stateGlobal.windowId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
|
|||||||
class _InstallPageState extends State<InstallPage> {
|
class _InstallPageState extends State<InstallPage> {
|
||||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||||
|
|
||||||
@override
|
_InstallPageState() {
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
Get.put<DesktopTabController>(tabController);
|
Get.put<DesktopTabController>(tabController);
|
||||||
const label = "install";
|
const label = "install";
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DragToResizeArea(
|
return DragToResizeArea(
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
|||||||
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
controller = TextEditingController(text: bind.installInstallPath());
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +147,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.all(0.75 * em),
|
contentPadding: EdgeInsets.all(0.75 * em),
|
||||||
),
|
),
|
||||||
).marginOnly(right: 10),
|
).workaroundFreezeLinuxMint().marginOnly(right: 10),
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(
|
||||||
() => OutlinedButton.icon(
|
() => OutlinedButton.icon(
|
||||||
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
|||||||
if (desktopicon.value) args += ' desktopicon';
|
if (desktopicon.value) args += ' desktopicon';
|
||||||
bind.installInstallMe(options: args, path: controller.text);
|
bind.installInstallMe(options: args, path: controller.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_install();
|
do_install();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget {
|
|||||||
required this.isRDP,
|
required this.isRDP,
|
||||||
required this.isSharedPassword,
|
required this.isSharedPassword,
|
||||||
this.forceRelay,
|
this.forceRelay,
|
||||||
|
this.connToken,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
final String id;
|
final String id;
|
||||||
final String? password;
|
final String? password;
|
||||||
@@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget {
|
|||||||
final bool isRDP;
|
final bool isRDP;
|
||||||
final bool? forceRelay;
|
final bool? forceRelay;
|
||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
|
final String? connToken;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||||
@@ -62,10 +64,14 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
password: widget.password,
|
password: widget.password,
|
||||||
isSharedPassword: widget.isSharedPassword,
|
isSharedPassword: widget.isSharedPassword,
|
||||||
forceRelay: widget.forceRelay,
|
forceRelay: widget.forceRelay,
|
||||||
|
connToken: widget.connToken,
|
||||||
isRdp: widget.isRDP);
|
isRdp: widget.isRDP);
|
||||||
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||||
debugPrint("Port forward page init success with id ${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
|
@override
|
||||||
@@ -232,7 +238,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
))),
|
)).workaroundFreezeLinuxMint()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(windowId())
|
||||||
.setTitle(getWindowNameWithId(id));
|
.setTitle(getWindowNameWithId(id));
|
||||||
};
|
};
|
||||||
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: params['id'],
|
key: params['id'],
|
||||||
label: params['id'],
|
label: params['id'],
|
||||||
@@ -47,6 +48,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
isRDP: isRDP,
|
isRDP: isRDP,
|
||||||
forceRelay: params['forceRelay'],
|
forceRelay: params['forceRelay'],
|
||||||
|
connToken: params['connToken'],
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +56,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
|
||||||
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||||
@@ -83,6 +83,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
isRDP: isRDP,
|
isRDP: isRDP,
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
forceRelay: args['forceRelay'],
|
forceRelay: args['forceRelay'],
|
||||||
|
connToken: args['connToken'],
|
||||||
)));
|
)));
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
@@ -106,6 +107,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
tail: AddButton(),
|
tail: AddButton(),
|
||||||
|
selectedBorderColor: MyTheme.accent,
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -127,6 +129,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
() => SubWindowDragToResizeArea(
|
() => SubWindowDragToResizeArea(
|
||||||
child: tabWidget,
|
child: tabWidget,
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||||
windowId: stateGlobal.windowId,
|
windowId: stateGlobal.windowId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.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 'package:flutter_hbb/models/state_model.dart';
|
||||||
|
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
@@ -45,7 +44,9 @@ class RemotePage extends StatefulWidget {
|
|||||||
this.switchUuid,
|
this.switchUuid,
|
||||||
this.forceRelay,
|
this.forceRelay,
|
||||||
this.isSharedPassword,
|
this.isSharedPassword,
|
||||||
}) : super(key: key);
|
}) : super(key: key) {
|
||||||
|
initSharedStates(id);
|
||||||
|
}
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final SessionID? sessionId;
|
final SessionID? sessionId;
|
||||||
@@ -64,7 +65,7 @@ class RemotePage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<RemotePage> createState() {
|
State<RemotePage> createState() {
|
||||||
final state = _RemotePageState();
|
final state = _RemotePageState(id);
|
||||||
_lastState.value = state;
|
_lastState.value = state;
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -94,8 +95,11 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
SessionID get sessionId => _ffi.sessionId;
|
SessionID get sessionId => _ffi.sessionId;
|
||||||
|
|
||||||
|
_RemotePageState(String id) {
|
||||||
|
_initStates(id);
|
||||||
|
}
|
||||||
|
|
||||||
void _initStates(String id) {
|
void _initStates(String id) {
|
||||||
initSharedStates(id);
|
|
||||||
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||||
@@ -105,12 +109,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initStates(widget.id);
|
|
||||||
_ffi = FFI(widget.sessionId);
|
_ffi = FFI(widget.sessionId);
|
||||||
Get.put<FFI>(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
|
_ffi.recordingModel
|
||||||
|
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
||||||
});
|
});
|
||||||
_ffi.start(
|
_ffi.start(
|
||||||
widget.id,
|
widget.id,
|
||||||
@@ -135,11 +140,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
// Session option should be set after models.dart/FFI.start
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
// Session option should be set after models.dart/FFI.start
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
sessionId: sessionId, arg: kOptionZoomCursor);
|
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
|
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||||
|
});
|
||||||
DesktopMultiWindow.addListener(this);
|
DesktopMultiWindow.addListener(this);
|
||||||
// if (!_isCustomCursorInited) {
|
// if (!_isCustomCursorInited) {
|
||||||
// customCursorController.registerNeedUpdateCursorCallback(
|
// customCursorController.registerNeedUpdateCursorCallback(
|
||||||
@@ -154,7 +161,10 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
_blockableOverlayState.applyFfi(_ffi);
|
_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
|
@override
|
||||||
@@ -236,13 +246,14 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||||
// ensure we leave this session, this is a double check
|
if (closeSession) {
|
||||||
_ffi.inputModel.enterOrLeave(false);
|
// ensure we leave this session, this is a double check
|
||||||
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
|
}
|
||||||
DesktopMultiWindow.removeListener(this);
|
DesktopMultiWindow.removeListener(this);
|
||||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||||
_ffi.imageModel.disposeImage();
|
_ffi.imageModel.disposeImage();
|
||||||
_ffi.cursorModel.disposeImages();
|
_ffi.cursorModel.disposeImages();
|
||||||
_ffi.recordingModel.onClose();
|
|
||||||
_rawKeyFocusNode.dispose();
|
_rawKeyFocusNode.dispose();
|
||||||
await _ffi.close(closeSession: closeSession);
|
await _ffi.close(closeSession: closeSession);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
@@ -565,11 +576,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
|
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
|
||||||
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final m = Provider.of<ImageModel>(context);
|
final m = Provider.of<ImageModel>(context);
|
||||||
@@ -735,12 +741,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
ScrollController horizontal,
|
ScrollController horizontal,
|
||||||
ScrollController vertical,
|
ScrollController vertical,
|
||||||
) {
|
) {
|
||||||
final scrollConfig = CustomMouseWheelScrollConfig(
|
|
||||||
scrollDuration: kDefaultScrollDuration,
|
|
||||||
scrollCurve: Curves.linearToEaseOut,
|
|
||||||
mouseWheelTurnsThrottleTimeMs:
|
|
||||||
kDefaultMouseWheelThrottleDuration.inMilliseconds,
|
|
||||||
scrollAmountMultiplier: kDefaultScrollAmountMultiplier);
|
|
||||||
var widget = child;
|
var widget = child;
|
||||||
if (layoutSize.width < size.width) {
|
if (layoutSize.width < size.width) {
|
||||||
widget = ScrollConfiguration(
|
widget = ScrollConfiguration(
|
||||||
@@ -786,36 +786,26 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (layoutSize.width < size.width) {
|
if (layoutSize.width < size.width) {
|
||||||
widget = ImprovedScrolling(
|
widget = RawScrollbar(
|
||||||
scrollController: horizontal,
|
thickness: kScrollbarThickness,
|
||||||
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
|
thumbColor: Colors.grey,
|
||||||
customMouseWheelScrollConfig: scrollConfig,
|
controller: horizontal,
|
||||||
child: RawScrollbar(
|
thumbVisibility: false,
|
||||||
thickness: kScrollbarThickness,
|
trackVisibility: false,
|
||||||
thumbColor: Colors.grey,
|
notificationPredicate: layoutSize.height < size.height
|
||||||
controller: horizontal,
|
? (notification) => notification.depth == 1
|
||||||
thumbVisibility: false,
|
: defaultScrollNotificationPredicate,
|
||||||
trackVisibility: false,
|
child: widget,
|
||||||
notificationPredicate: layoutSize.height < size.height
|
|
||||||
? (notification) => notification.depth == 1
|
|
||||||
: defaultScrollNotificationPredicate,
|
|
||||||
child: widget,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (layoutSize.height < size.height) {
|
if (layoutSize.height < size.height) {
|
||||||
widget = ImprovedScrolling(
|
widget = RawScrollbar(
|
||||||
scrollController: vertical,
|
thickness: kScrollbarThickness,
|
||||||
enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
|
thumbColor: Colors.grey,
|
||||||
customMouseWheelScrollConfig: scrollConfig,
|
controller: vertical,
|
||||||
child: RawScrollbar(
|
thumbVisibility: false,
|
||||||
thickness: kScrollbarThickness,
|
trackVisibility: false,
|
||||||
thumbColor: Colors.grey,
|
child: widget,
|
||||||
controller: vertical,
|
|
||||||
thumbVisibility: false,
|
|
||||||
trackVisibility: false,
|
|
||||||
child: widget,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final ffi = remotePage.ffi;
|
final ffi = remotePage.ffi;
|
||||||
bind.setCurSessionId(sessionId: ffi.sessionId);
|
bind.setCurSessionId(sessionId: ffi.sessionId);
|
||||||
}
|
}
|
||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(params['windowId'])
|
||||||
.setTitle(getWindowNameWithId(id));
|
.setTitle(getWindowNameWithId(id));
|
||||||
UnreadChatCountState.find(id).value = 0;
|
UnreadChatCountState.find(id).value = 0;
|
||||||
};
|
};
|
||||||
@@ -98,15 +98,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
));
|
));
|
||||||
_update_remote_count();
|
_update_remote_count();
|
||||||
}
|
}
|
||||||
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||||
|
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
|
||||||
|
|
||||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
|
||||||
if (!_isScreenRectSet) {
|
if (!_isScreenRectSet) {
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
restoreWindowPosition(
|
restoreWindowPosition(
|
||||||
@@ -121,11 +120,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final child = Scaffold(
|
final child = Scaffold(
|
||||||
@@ -134,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
controller: tabController,
|
controller: tabController,
|
||||||
onWindowCloseButton: handleWindowCloseButton,
|
onWindowCloseButton: handleWindowCloseButton,
|
||||||
tail: const AddButton(),
|
tail: const AddButton(),
|
||||||
|
selectedBorderColor: MyTheme.accent,
|
||||||
pageViewBuilder: (pageView) => pageView,
|
pageViewBuilder: (pageView) => pageView,
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||||
@@ -233,6 +228,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
// Specially configured for a better resize area and remote control.
|
// Specially configured for a better resize area and remote control.
|
||||||
childPadding: kDragToResizeAreaPadding,
|
childPadding: kDragToResizeAreaPadding,
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||||
windowId: stateGlobal.windowId,
|
windowId: stateGlobal.windowId,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -399,7 +395,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
RemoteCountState.find().value = tabController.length;
|
RemoteCountState.find().value = tabController.length;
|
||||||
|
|
||||||
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
||||||
print(
|
debugPrint(
|
||||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||||
|
|
||||||
dynamic returnValue;
|
dynamic returnValue;
|
||||||
@@ -413,12 +409,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final display = args['display'];
|
final display = args['display'];
|
||||||
final displays = args['displays'];
|
final displays = args['displays'];
|
||||||
final screenRect = parseParamScreenRect(args);
|
final screenRect = parseParamScreenRect(args);
|
||||||
|
final prePeerCount = tabController.length;
|
||||||
Future.delayed(Duration.zero, () async {
|
Future.delayed(Duration.zero, () async {
|
||||||
if (stateGlobal.fullscreen.isTrue) {
|
if (stateGlobal.fullscreen.isTrue) {
|
||||||
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
}
|
}
|
||||||
await setNewConnectWindowFrame(windowId(), id!, display, screenRect);
|
await setNewConnectWindowFrame(
|
||||||
|
windowId(), id!, prePeerCount, display, screenRect);
|
||||||
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||||
await windowOnTop(windowId());
|
await windowOnTop(windowId());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,14 +32,18 @@ class DesktopServerPage extends StatefulWidget {
|
|||||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||||
with WindowListener, AutomaticKeepAliveClientMixin {
|
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
final tabController = gFFI.serverModel.tabController;
|
final tabController = gFFI.serverModel.tabController;
|
||||||
@override
|
|
||||||
void initState() {
|
_DesktopServerPageState() {
|
||||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
||||||
windowManager.addListener(this);
|
|
||||||
Get.put<DesktopTabController>(tabController);
|
Get.put<DesktopTabController>(tabController);
|
||||||
tabController.onRemoved = (_, id) {
|
tabController.onRemoved = (_, id) {
|
||||||
onRemoveId(id);
|
onRemoveId(id);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
windowManager.addListener(this);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +83,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
child: Consumer<ServerModel>(
|
child: Consumer<ServerModel>(
|
||||||
builder: (context, serverModel, child) {
|
builder: (context, serverModel, child) {
|
||||||
final body = Scaffold(
|
final body = Scaffold(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
body: ConnectionManager(),
|
body: ConnectionManager(),
|
||||||
);
|
);
|
||||||
return isLinux
|
return isLinux
|
||||||
@@ -106,21 +110,10 @@ class ConnectionManager extends StatefulWidget {
|
|||||||
|
|
||||||
class ConnectionManagerState extends State<ConnectionManager>
|
class ConnectionManagerState extends State<ConnectionManager>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
final RxBool _block = false.obs;
|
final RxBool _controlPageBlock = false.obs;
|
||||||
|
final RxBool _sidePageBlock = false.obs;
|
||||||
|
|
||||||
@override
|
ConnectionManagerState() {
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
||||||
super.didChangeAppLifecycleState(state);
|
|
||||||
if (state == AppLifecycleState.resumed) {
|
|
||||||
if (!allowRemoteCMModification()) {
|
|
||||||
shouldBeBlocked(_block, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
gFFI.serverModel.updateClientState();
|
|
||||||
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
||||||
final client_id = int.tryParse(client_id_str);
|
final client_id = int.tryParse(client_id_str);
|
||||||
if (client_id != null) {
|
if (client_id != null) {
|
||||||
@@ -129,7 +122,7 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
if (client != null) {
|
if (client != null) {
|
||||||
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
||||||
if (client.unreadChatMessageCount.value > 0) {
|
if (client.unreadChatMessageCount.value > 0) {
|
||||||
Future.delayed(Duration.zero, () {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
client.unreadChatMessageCount.value = 0;
|
client.unreadChatMessageCount.value = 0;
|
||||||
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
||||||
});
|
});
|
||||||
@@ -140,6 +133,22 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
gFFI.chatModel.isConnManager = true;
|
gFFI.chatModel.isConnManager = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
if (!allowRemoteCMModification()) {
|
||||||
|
shouldBeBlocked(_controlPageBlock, null);
|
||||||
|
shouldBeBlocked(_sidePageBlock, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
gFFI.serverModel.updateClientState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
@@ -185,9 +194,6 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
selectedBorderColor: MyTheme.accent,
|
selectedBorderColor: MyTheme.accent,
|
||||||
maxLabelWidth: 100,
|
maxLabelWidth: 100,
|
||||||
tail: null, //buildScrollJumper(),
|
tail: null, //buildScrollJumper(),
|
||||||
blockTab: allowRemoteCMModification() ? null : _block,
|
|
||||||
selectedTabBackgroundColor:
|
|
||||||
Theme.of(context).hintColor.withOpacity(0),
|
|
||||||
tabBuilder: (key, icon, label, themeConf) {
|
tabBuilder: (key, icon, label, themeConf) {
|
||||||
final client = serverModel.clients
|
final client = serverModel.clients
|
||||||
.firstWhereOrNull((client) => client.id.toString() == key);
|
.firstWhereOrNull((client) => client.id.toString() == key);
|
||||||
@@ -222,7 +228,7 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
borderWidth;
|
borderWidth;
|
||||||
final realChatPageWidth =
|
final realChatPageWidth =
|
||||||
constrains.maxWidth - realClosedWidth;
|
constrains.maxWidth - realClosedWidth;
|
||||||
return Row(children: [
|
final row = Row(children: [
|
||||||
if (constrains.maxWidth >
|
if (constrains.maxWidth >
|
||||||
kConnectionManagerWindowSizeClosedChat.width)
|
kConnectionManagerWindowSizeClosedChat.width)
|
||||||
Consumer<ChatModel>(
|
Consumer<ChatModel>(
|
||||||
@@ -232,14 +238,25 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
? buildSidePage()
|
? buildSidePage()
|
||||||
: buildRemoteBlock(
|
: buildRemoteBlock(
|
||||||
child: buildSidePage(),
|
child: buildSidePage(),
|
||||||
block: _block,
|
block: _sidePageBlock,
|
||||||
mask: true),
|
mask: true),
|
||||||
)),
|
)),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: realClosedWidth,
|
width: realClosedWidth,
|
||||||
child:
|
child: SizedBox(
|
||||||
SizedBox(width: realClosedWidth, child: pageView)),
|
width: realClosedWidth,
|
||||||
|
child: allowRemoteCMModification()
|
||||||
|
? pageView
|
||||||
|
: buildRemoteBlock(
|
||||||
|
child: _buildKeyEventBlock(pageView),
|
||||||
|
block: _controlPageBlock,
|
||||||
|
mask: false,
|
||||||
|
))),
|
||||||
]);
|
]);
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: row,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -259,6 +276,10 @@ class ConnectionManagerState extends State<ConnectionManager>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildKeyEventBlock(Widget child) {
|
||||||
|
return ExcludeFocus(child: child, excluding: true);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildTitleBar() {
|
Widget buildTitleBar() {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: kDesktopRemoteTabBarHeight,
|
height: kDesktopRemoteTabBarHeight,
|
||||||
@@ -399,7 +420,10 @@ class _CmHeaderState extends State<_CmHeader>
|
|||||||
_time.value = _time.value + 1;
|
_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
|
@override
|
||||||
@@ -732,7 +756,8 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
child: buildButton(context,
|
child: buildButton(context,
|
||||||
color: MyTheme.accent,
|
color: MyTheme.accent,
|
||||||
onClick: null, onTapDown: (details) async {
|
onClick: null, onTapDown: (details) async {
|
||||||
final devicesInfo = await AudioInput.getDevicesInfo(true, true);
|
final devicesInfo =
|
||||||
|
await AudioInput.getDevicesInfo(true, true);
|
||||||
List<String> devices = devicesInfo['devices'] as List<String>;
|
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
msgBox(
|
msgBox(
|
||||||
@@ -764,7 +789,8 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
value: d,
|
value: d,
|
||||||
groupValue: currentDevice,
|
groupValue: currentDevice,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
if (v != null) AudioInput.setDevice(v, true, true);
|
if (v != null)
|
||||||
|
AudioInput.setDevice(v, true, true);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1143,6 +1169,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
|||||||
Text(translate('Create Folder'))
|
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'))
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
|
|||||||
localPlatform = kPeerPlatformWindows;
|
localPlatform = kPeerPlatformWindows;
|
||||||
} else if (isLinux) {
|
} else if (isLinux) {
|
||||||
localPlatform = kPeerPlatformLinux;
|
localPlatform = kPeerPlatformLinux;
|
||||||
|
} else if (isWebOnWindows || isWebOnLinux) {
|
||||||
|
localPlatform = kPeerPlatformWebDesktop;
|
||||||
}
|
}
|
||||||
// to-do: web desktop support ?
|
|
||||||
return localPlatform;
|
return localPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 300),
|
||||||
message: widget.tooltip,
|
message: widget.tooltip,
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
|
|||||||
@@ -38,18 +38,16 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
||||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
|
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(enabled?.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||||
extends State<W> {
|
extends State<W> {
|
||||||
RxBool enabled = true.obs;
|
RxBool enabled = true.obs;
|
||||||
|
|
||||||
@override
|
MyPopupMenuItemState(bool? e) {
|
||||||
void initState() {
|
if (e != null) {
|
||||||
super.initState();
|
enabled.value = e;
|
||||||
if (widget.enabled != null) {
|
|
||||||
enabled.value = widget.enabled!.value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ class RemoteMenuEntry {
|
|||||||
}) {
|
}) {
|
||||||
return MenuEntryButton<String>(
|
return MenuEntryButton<String>(
|
||||||
childBuilder: (TextStyle? style) => Text(
|
childBuilder: (TextStyle? style) => Text(
|
||||||
'${translate("Insert")} Ctrl + Alt + Del',
|
translate("Insert Ctrl + Alt + Del"),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
proc: () {
|
proc: () {
|
||||||
@@ -372,7 +372,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
Future.delayed(Duration.zero, () async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
_fractionX.value = double.tryParse(await bind.sessionGetOption(
|
_fractionX.value = double.tryParse(await bind.sessionGetOption(
|
||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
arg: 'remote-menubar-drag-x') ??
|
arg: 'remote-menubar-drag-x') ??
|
||||||
@@ -436,6 +436,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
shadowColor: MyTheme.color(context).shadow,
|
shadowColor: MyTheme.color(context).shadow,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: _DraggableShowHide(
|
child: _DraggableShowHide(
|
||||||
|
id: widget.id,
|
||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
dragging: _dragging,
|
dragging: _dragging,
|
||||||
fractionX: _fractionX,
|
fractionX: _fractionX,
|
||||||
@@ -452,8 +453,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
|
|
||||||
Widget _buildToolbar(BuildContext context) {
|
Widget _buildToolbar(BuildContext context) {
|
||||||
final List<Widget> toolbarItems = [];
|
final List<Widget> toolbarItems = [];
|
||||||
|
toolbarItems.add(_PinMenu(state: widget.state));
|
||||||
if (!isWebDesktop) {
|
if (!isWebDesktop) {
|
||||||
toolbarItems.add(_PinMenu(state: widget.state));
|
|
||||||
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,8 +479,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
setFullscreen: _setFullscreen,
|
setFullscreen: _setFullscreen,
|
||||||
));
|
));
|
||||||
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
|
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
|
||||||
|
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||||
if (!isWeb) {
|
if (!isWeb) {
|
||||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
|
||||||
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
||||||
}
|
}
|
||||||
if (!isWeb) toolbarItems.add(_RecordMenu());
|
if (!isWeb) toolbarItems.add(_RecordMenu());
|
||||||
@@ -1032,11 +1033,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
FFI get ffi => widget.ffi;
|
FFI get ffi => widget.ffi;
|
||||||
String get id => widget.id;
|
String get id => widget.id;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_screenAdjustor.updateScreen();
|
_screenAdjustor.updateScreen();
|
||||||
@@ -1278,7 +1274,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_getLocalResolutionWayland();
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_getLocalResolutionWayland();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect? scaledRect() {
|
Rect? scaledRect() {
|
||||||
@@ -1497,7 +1495,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField _resolutionInput(TextEditingController controller) {
|
Widget _resolutionInput(TextEditingController controller) {
|
||||||
return TextField(
|
return TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
@@ -1511,7 +1509,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
||||||
],
|
],
|
||||||
controller: controller,
|
controller: controller,
|
||||||
);
|
).workaroundFreezeLinuxMint();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _supportedResolutionMenuButtons() => resolutions
|
List<Widget> _supportedResolutionMenuButtons() => resolutions
|
||||||
@@ -1615,7 +1613,9 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
// If use flutter to grab keys, we can only use one mode.
|
// If use flutter to grab keys, we can only use one mode.
|
||||||
// Map mode and Legacy mode, at least one of them is supported.
|
// Map mode and Legacy mode, at least one of them is supported.
|
||||||
String? modeOnly;
|
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(
|
if (bind.sessionIsKeyboardModeSupported(
|
||||||
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
||||||
modeOnly = kKeyMapMode;
|
modeOnly = kKeyMapMode;
|
||||||
@@ -1719,7 +1719,9 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionToggleOption(
|
await bind.sessionToggleOption(
|
||||||
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||||
ffiModel.setViewOnly(id, value);
|
final viewOnly = await bind.sessionGetToggleOption(
|
||||||
|
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
|
||||||
|
ffiModel.setViewOnly(id, viewOnly ?? value);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
@@ -1779,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _IconSubmenuButton(
|
if (isWeb) {
|
||||||
tooltip: 'Chat',
|
return buildTextChatButton();
|
||||||
key: chatButtonKey,
|
} else {
|
||||||
svg: 'assets/chat.svg',
|
return _IconSubmenuButton(
|
||||||
ffi: widget.ffi,
|
tooltip: 'Chat',
|
||||||
color: _ToolbarTheme.blueColor,
|
key: chatButtonKey,
|
||||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
svg: 'assets/chat.svg',
|
||||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
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() {
|
textChat() {
|
||||||
return MenuButton(
|
return MenuButton(
|
||||||
child: Text(translate('Text chat')),
|
child: Text(translate('Text chat')),
|
||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
onPressed: () {
|
onPressed: _textChatOnPressed);
|
||||||
RenderBox? renderBox =
|
}
|
||||||
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
|
|
||||||
Offset? initPos;
|
_textChatOnPressed() {
|
||||||
if (renderBox != null) {
|
RenderBox? renderBox =
|
||||||
final pos = renderBox.localToGlobal(Offset.zero);
|
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
Offset? initPos;
|
||||||
}
|
if (renderBox != null) {
|
||||||
|
final pos = renderBox.localToGlobal(Offset.zero);
|
||||||
widget.ffi.chatModel.changeCurrentKey(
|
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
||||||
MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
}
|
||||||
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
widget.ffi.chatModel
|
||||||
});
|
.changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
||||||
|
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
voiceCall() {
|
voiceCall() {
|
||||||
@@ -1907,8 +1924,7 @@ class _RecordMenu extends StatelessWidget {
|
|||||||
var ffi = Provider.of<FfiModel>(context);
|
var ffi = Provider.of<FfiModel>(context);
|
||||||
var recordingModel = Provider.of<RecordingModel>(context);
|
var recordingModel = Provider.of<RecordingModel>(context);
|
||||||
final visible =
|
final visible =
|
||||||
(recordingModel.start || ffi.permissions['recording'] != false) &&
|
(recordingModel.start || ffi.permissions['recording'] != false);
|
||||||
ffi.pi.currentDisplay != kAllDisplayValue;
|
|
||||||
if (!visible) return Offstage();
|
if (!visible) return Offstage();
|
||||||
return _IconMenuButton(
|
return _IconMenuButton(
|
||||||
assetName: 'assets/rec.svg',
|
assetName: 'assets/rec.svg',
|
||||||
@@ -2217,6 +2233,7 @@ class RdoMenuButton<T> extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableShowHide extends StatefulWidget {
|
class _DraggableShowHide extends StatefulWidget {
|
||||||
|
final String id;
|
||||||
final SessionID sessionId;
|
final SessionID sessionId;
|
||||||
final RxDouble fractionX;
|
final RxDouble fractionX;
|
||||||
final RxBool dragging;
|
final RxBool dragging;
|
||||||
@@ -2228,6 +2245,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
|
|
||||||
const _DraggableShowHide({
|
const _DraggableShowHide({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
required this.id,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.fractionX,
|
required this.fractionX,
|
||||||
required this.dragging,
|
required this.dragging,
|
||||||
@@ -2317,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
);
|
);
|
||||||
final isFullscreen = stateGlobal.fullscreen;
|
final isFullscreen = stateGlobal.fullscreen;
|
||||||
const double iconSize = 20;
|
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(
|
final child = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildDraggable(context),
|
_buildDraggable(context),
|
||||||
Obx(() => TextButton(
|
Obx(() => buttonWrapper(
|
||||||
onPressed: () {
|
() {
|
||||||
widget.setFullscreen(!isFullscreen.value);
|
widget.setFullscreen(!isFullscreen.value);
|
||||||
},
|
},
|
||||||
child: Tooltip(
|
Tooltip(
|
||||||
message: translate(
|
message: translate(
|
||||||
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
|
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -2336,12 +2372,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
if (!isMacOS)
|
if (!isMacOS && !isWebDesktop)
|
||||||
Obx(() => Offstage(
|
Obx(() => Offstage(
|
||||||
offstage: isFullscreen.isFalse,
|
offstage: isFullscreen.isFalse,
|
||||||
child: TextButton(
|
child: buttonWrapper(
|
||||||
onPressed: () => widget.setMinimize(),
|
widget.setMinimize,
|
||||||
child: Tooltip(
|
Tooltip(
|
||||||
message: translate('Minimize'),
|
message: translate('Minimize'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.remove,
|
Icons.remove,
|
||||||
@@ -2350,11 +2386,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
TextButton(
|
buttonWrapper(
|
||||||
onPressed: () => setState(() {
|
() => setState(() {
|
||||||
widget.toolbarState.switchShow(widget.sessionId);
|
widget.toolbarState.switchShow(widget.sessionId);
|
||||||
}),
|
}),
|
||||||
child: Obx((() => Tooltip(
|
Obx((() => Tooltip(
|
||||||
message:
|
message:
|
||||||
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -2363,6 +2399,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
))),
|
))),
|
||||||
),
|
),
|
||||||
|
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(
|
return TextButtonTheme(
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -227,8 +227,7 @@ typedef TabMenuBuilder = Widget Function(String key);
|
|||||||
typedef LabelGetter = Rx<String> Function(String key);
|
typedef LabelGetter = Rx<String> Function(String key);
|
||||||
|
|
||||||
/// [_lastClickTime], help to handle double click
|
/// [_lastClickTime], help to handle double click
|
||||||
int _lastClickTime =
|
int _lastClickTime = 0;
|
||||||
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
|
||||||
|
|
||||||
class DesktopTab extends StatefulWidget {
|
class DesktopTab extends StatefulWidget {
|
||||||
final bool showLogo;
|
final bool showLogo;
|
||||||
@@ -247,7 +246,6 @@ class DesktopTab extends StatefulWidget {
|
|||||||
final Color? selectedTabBackgroundColor;
|
final Color? selectedTabBackgroundColor;
|
||||||
final Color? unSelectedTabBackgroundColor;
|
final Color? unSelectedTabBackgroundColor;
|
||||||
final Color? selectedBorderColor;
|
final Color? selectedBorderColor;
|
||||||
final RxBool? blockTab;
|
|
||||||
|
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
|
|
||||||
@@ -273,7 +271,6 @@ class DesktopTab extends StatefulWidget {
|
|||||||
this.selectedTabBackgroundColor,
|
this.selectedTabBackgroundColor,
|
||||||
this.unSelectedTabBackgroundColor,
|
this.unSelectedTabBackgroundColor,
|
||||||
this.selectedBorderColor,
|
this.selectedBorderColor,
|
||||||
this.blockTab,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
static RxString tablabelGetter(String peerId) {
|
static RxString tablabelGetter(String peerId) {
|
||||||
@@ -312,7 +309,6 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
Color? get unSelectedTabBackgroundColor =>
|
Color? get unSelectedTabBackgroundColor =>
|
||||||
widget.unSelectedTabBackgroundColor;
|
widget.unSelectedTabBackgroundColor;
|
||||||
Color? get selectedBorderColor => widget.selectedBorderColor;
|
Color? get selectedBorderColor => widget.selectedBorderColor;
|
||||||
RxBool? get blockTab => widget.blockTab;
|
|
||||||
DesktopTabController get controller => widget.controller;
|
DesktopTabController get controller => widget.controller;
|
||||||
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
|
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
|
||||||
Debouncer get _scrollDebounce => widget._scrollDebounce;
|
Debouncer get _scrollDebounce => widget._scrollDebounce;
|
||||||
@@ -506,17 +502,20 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
Obx(() {
|
Obx(() {
|
||||||
if (stateGlobal.showTabBar.isTrue &&
|
if (stateGlobal.showTabBar.isTrue &&
|
||||||
!(kUseCompatibleUiMode && isHideSingleItem())) {
|
!(kUseCompatibleUiMode && isHideSingleItem())) {
|
||||||
|
final showBottomDivider = _showTabBarBottomDivider(tabType);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: _kTabBarHeight,
|
height: _kTabBarHeight,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: _kTabBarHeight - 1,
|
height:
|
||||||
|
showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight,
|
||||||
child: _buildBar(),
|
child: _buildBar(),
|
||||||
),
|
),
|
||||||
const Divider(
|
if (showBottomDivider)
|
||||||
height: 1,
|
const Divider(
|
||||||
),
|
height: 1,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -531,25 +530,20 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBlock({required Widget child}) {
|
|
||||||
if (blockTab != null) {
|
|
||||||
return buildRemoteBlock(
|
|
||||||
child: child,
|
|
||||||
block: blockTab!,
|
|
||||||
use: canBeBlocked,
|
|
||||||
mask: tabType == DesktopTabType.main);
|
|
||||||
} else {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _tabWidgets = [];
|
List<Widget> _tabWidgets = [];
|
||||||
Widget _buildPageView() {
|
Widget _buildPageView() {
|
||||||
final child = _buildBlock(
|
final child = Container(
|
||||||
child: Obx(() => PageView(
|
child: Obx(() => PageView(
|
||||||
controller: state.value.pageController,
|
controller: state.value.pageController,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
children: () {
|
children: () {
|
||||||
|
if (DesktopTabType.cm == tabType) {
|
||||||
|
// Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful.
|
||||||
|
return state.value.tabs.map((tab) {
|
||||||
|
return tab.page;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
/// to-do refactor, separate connection state and UI state for remote session.
|
/// to-do refactor, separate connection state and UI state for remote session.
|
||||||
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
||||||
final tabLen = state.value.tabs.length;
|
final tabLen = state.value.tabs.length;
|
||||||
@@ -727,16 +721,6 @@ class WindowActionPanel extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WindowActionPanelState extends State<WindowActionPanel> {
|
class WindowActionPanelState extends State<WindowActionPanel> {
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool showTabDowndown() {
|
bool showTabDowndown() {
|
||||||
return widget.tabController.state.value.tabs.length > 1 &&
|
return widget.tabController.state.value.tabs.length > 1 &&
|
||||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||||
@@ -1172,7 +1156,10 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: _kTabBarHeight,
|
// _kTabBarHeight also displays normally
|
||||||
|
height: _showTabBarBottomDivider(widget.tabType)
|
||||||
|
? _kTabBarHeight - 1
|
||||||
|
: _kTabBarHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -1271,13 +1258,7 @@ class ActionIcon extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ActionIconState extends State<ActionIcon> {
|
class _ActionIconState extends State<ActionIcon> {
|
||||||
var hover = false.obs;
|
final hover = false.obs;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
hover.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1431,6 +1412,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _showTabBarBottomDivider(DesktopTabType tabType) {
|
||||||
|
return tabType == DesktopTabType.main || tabType == DesktopTabType.install;
|
||||||
|
}
|
||||||
|
|
||||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||||
final Color? selectedTabIconColor;
|
final Color? selectedTabIconColor;
|
||||||
final Color? unSelectedTabIconColor;
|
final Color? unSelectedTabIconColor;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ WindowType? kWindowType;
|
|||||||
late List<String> kBootArgs;
|
late List<String> kBootArgs;
|
||||||
|
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
|
earlyAssert();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
debugPrint("launch args: $args");
|
debugPrint("launch args: $args");
|
||||||
@@ -119,6 +120,7 @@ Future<void> initEnv(String appType) async {
|
|||||||
void runMainApp(bool startService) async {
|
void runMainApp(bool startService) async {
|
||||||
// register uni links
|
// register uni links
|
||||||
await initEnv(kAppTypeMain);
|
await initEnv(kAppTypeMain);
|
||||||
|
checkUpdate();
|
||||||
// trigger connection status updater
|
// trigger connection status updater
|
||||||
await bind.mainCheckConnectStatus();
|
await bind.mainCheckConnectStatus();
|
||||||
if (startService) {
|
if (startService) {
|
||||||
@@ -155,13 +157,14 @@ void runMainApp(bool startService) async {
|
|||||||
|
|
||||||
void runMobileApp() async {
|
void runMobileApp() async {
|
||||||
await initEnv(kAppTypeMain);
|
await initEnv(kAppTypeMain);
|
||||||
|
checkUpdate();
|
||||||
if (isAndroid) androidChannelInit();
|
if (isAndroid) androidChannelInit();
|
||||||
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||||
draggablePositions.load();
|
draggablePositions.load();
|
||||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
runApp(App());
|
runApp(App());
|
||||||
if (!isWeb) await initUniLinks();
|
await initUniLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
void runMultiWindow(
|
void runMultiWindow(
|
||||||
@@ -260,7 +263,7 @@ showCmWindow({bool isStartup = false}) async {
|
|||||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
||||||
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||||
bind.mainHideDocker();
|
bind.mainHideDock();
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
windowManager.show(),
|
windowManager.show(),
|
||||||
windowManager.focus(),
|
windowManager.focus(),
|
||||||
@@ -288,14 +291,14 @@ hideCmWindow({bool isStartup = false}) async {
|
|||||||
size: kConnectionManagerWindowSizeClosedChat);
|
size: kConnectionManagerWindowSizeClosedChat);
|
||||||
windowManager.setOpacity(0);
|
windowManager.setOpacity(0);
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||||
bind.mainHideDocker();
|
bind.mainHideDock();
|
||||||
await windowManager.minimize();
|
await windowManager.minimize();
|
||||||
await windowManager.hide();
|
await windowManager.hide();
|
||||||
_isCmReadyToShow = true;
|
_isCmReadyToShow = true;
|
||||||
} else if (_isCmReadyToShow) {
|
} else if (_isCmReadyToShow) {
|
||||||
if (await windowManager.getOpacity() != 0) {
|
if (await windowManager.getOpacity() != 0) {
|
||||||
await windowManager.setOpacity(0);
|
await windowManager.setOpacity(0);
|
||||||
bind.mainHideDocker();
|
bind.mainHideDock();
|
||||||
await windowManager.minimize();
|
await windowManager.minimize();
|
||||||
await windowManager.hide();
|
await windowManager.hide();
|
||||||
}
|
}
|
||||||
@@ -372,7 +375,7 @@ class App extends StatefulWidget {
|
|||||||
State<App> createState() => _AppState();
|
State<App> createState() => _AppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppState extends State<App> {
|
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -396,6 +399,34 @@ class _AppState extends State<App> {
|
|||||||
bind.mainChangeTheme(dark: to.toShortString());
|
bind.mainChangeTheme(dark: to.toShortString());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeMetrics() {
|
||||||
|
_updateOrientation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateOrientation() {
|
||||||
|
if (isDesktop) return;
|
||||||
|
|
||||||
|
// Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
|
||||||
|
// my test (Flutter 3.19.6, Android 14) is always the reverse value.
|
||||||
|
// https://github.com/flutter/flutter/issues/60899
|
||||||
|
// stateGlobal.isPortrait.value =
|
||||||
|
// MediaQuery.of(context).orientation == Orientation.portrait;
|
||||||
|
|
||||||
|
final orientation = View.of(context).physicalSize.aspectRatio > 1
|
||||||
|
? Orientation.landscape
|
||||||
|
: Orientation.portrait;
|
||||||
|
stateGlobal.isPortrait.value = orientation == Orientation.portrait;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -416,7 +447,9 @@ class _AppState extends State<App> {
|
|||||||
child: GetMaterialApp(
|
child: GetMaterialApp(
|
||||||
navigatorKey: globalKey,
|
navigatorKey: globalKey,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'RustDesk',
|
title: isWeb
|
||||||
|
? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)'
|
||||||
|
: bind.mainGetAppNameSync(),
|
||||||
theme: MyTheme.lightTheme,
|
theme: MyTheme.lightTheme,
|
||||||
darkTheme: MyTheme.darkTheme,
|
darkTheme: MyTheme.darkTheme,
|
||||||
themeMode: MyTheme.currentThemeMode(),
|
themeMode: MyTheme.currentThemeMode(),
|
||||||
@@ -447,7 +480,8 @@ class _AppState extends State<App> {
|
|||||||
: (context, child) {
|
: (context, child) {
|
||||||
child = _keepScaleBuilder(context, child);
|
child = _keepScaleBuilder(context, child);
|
||||||
child = botToastBuilder(context, child);
|
child = botToastBuilder(context, child);
|
||||||
if (isDesktop && desktopType == DesktopType.main) {
|
if ((isDesktop && desktopType == DesktopType.main) ||
|
||||||
|
isWebDesktop) {
|
||||||
child = keyListenerBuilder(context, child);
|
child = keyListenerBuilder(context, child);
|
||||||
}
|
}
|
||||||
if (isLinux) {
|
if (isLinux) {
|
||||||
@@ -475,7 +509,7 @@ _registerEventHandler() {
|
|||||||
platformFFI.registerEventHandler('theme', 'theme', (evt) async {
|
platformFFI.registerEventHandler('theme', 'theme', (evt) async {
|
||||||
String? dark = evt['dark'];
|
String? dark = evt['dark'];
|
||||||
if (dark != null) {
|
if (dark != null) {
|
||||||
MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
|
await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
platformFFI.registerEventHandler('language', 'language', (_) async {
|
platformFFI.registerEventHandler('language', 'language', (_) async {
|
||||||
|
|||||||
@@ -3,25 +3,24 @@ import 'dart:async';
|
|||||||
import 'package:auto_size_text_field/auto_size_text_field.dart';
|
import 'package:auto_size_text_field/auto_size_text_field.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/widgets/login.dart';
|
|
||||||
import '../../common/widgets/peer_tab_page.dart';
|
import '../../common/widgets/peer_tab_page.dart';
|
||||||
import '../../common/widgets/autocomplete.dart';
|
import '../../common/widgets/autocomplete.dart';
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
import 'scan_page.dart';
|
|
||||||
import 'settings_page.dart';
|
|
||||||
|
|
||||||
/// Connection page for connecting to a remote peer.
|
/// Connection page for connecting to a remote peer.
|
||||||
class ConnectionPage extends StatefulWidget implements PageShape {
|
class ConnectionPage extends StatefulWidget implements PageShape {
|
||||||
ConnectionPage({Key? key}) : super(key: key);
|
ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final icon = const Icon(Icons.connected_tv);
|
final icon = const Icon(Icons.connected_tv);
|
||||||
@@ -30,7 +29,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
|
|||||||
final title = translate("Connection");
|
final title = translate("Connection");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
|
final List<Widget> appBarActions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ConnectionPage> createState() => _ConnectionPageState();
|
State<ConnectionPage> createState() => _ConnectionPageState();
|
||||||
@@ -42,41 +41,36 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
final _idController = IDTextEditingController();
|
final _idController = IDTextEditingController();
|
||||||
final RxBool _idEmpty = true.obs;
|
final RxBool _idEmpty = true.obs;
|
||||||
|
|
||||||
/// Update url. If it's not null, means an update is available.
|
|
||||||
var _updateUrl = '';
|
|
||||||
List<Peer> peers = [];
|
List<Peer> peers = [];
|
||||||
|
|
||||||
bool isPeersLoading = false;
|
bool isPeersLoading = false;
|
||||||
bool isPeersLoaded = false;
|
bool isPeersLoaded = false;
|
||||||
StreamSubscription? _uniLinksSubscription;
|
StreamSubscription? _uniLinksSubscription;
|
||||||
|
|
||||||
|
// https://github.com/flutter/flutter/issues/157244
|
||||||
|
Iterable<Peer> _autocompleteOpts = [];
|
||||||
|
|
||||||
|
_ConnectionPageState() {
|
||||||
|
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||||
|
_idController.addListener(() {
|
||||||
|
_idEmpty.value = _idController.text.isEmpty;
|
||||||
|
});
|
||||||
|
Get.put<IDTextEditingController>(_idController);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
|
||||||
if (_idController.text.isEmpty) {
|
if (_idController.text.isEmpty) {
|
||||||
() async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||||
if (lastRemoteId != _idController.id) {
|
if (lastRemoteId != _idController.id) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_idController.id = lastRemoteId;
|
_idController.id = lastRemoteId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}();
|
});
|
||||||
}
|
}
|
||||||
if (isAndroid) {
|
|
||||||
if (!bind.isCustomClient()) {
|
|
||||||
Timer(const Duration(seconds: 1), () async {
|
|
||||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
|
||||||
if (_updateUrl.isNotEmpty) setState(() {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_idController.addListener(() {
|
|
||||||
_idEmpty.value = _idController.text.isEmpty;
|
|
||||||
});
|
|
||||||
Get.put<IDTextEditingController>(_idController);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -86,7 +80,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
slivers: [
|
slivers: [
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildListDelegate([
|
delegate: SliverChildListDelegate([
|
||||||
if (!bind.isCustomClient()) _buildUpdateUI(),
|
if (!bind.isCustomClient() && !isIOS)
|
||||||
|
Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)),
|
||||||
_buildRemoteIDTextField(),
|
_buildRemoteIDTextField(),
|
||||||
])),
|
])),
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
@@ -105,16 +100,22 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// UI for software update.
|
/// UI for software update.
|
||||||
/// If [_updateUrl] is not empty, shows a button to update the software.
|
/// If _updateUrl] is not empty, shows a button to update the software.
|
||||||
Widget _buildUpdateUI() {
|
Widget _buildUpdateUI(String updateUrl) {
|
||||||
return _updateUrl.isEmpty
|
return updateUrl.isEmpty
|
||||||
? const SizedBox(height: 0)
|
? const SizedBox(height: 0)
|
||||||
: InkWell(
|
: InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final url = 'https://rustdesk.com/download';
|
final url = 'https://rustdesk.com/download';
|
||||||
if (await canLaunchUrl(Uri.parse(url))) {
|
// https://pub.dev/packages/url_launcher#configuration
|
||||||
await launchUrl(Uri.parse(url));
|
// https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs
|
||||||
}
|
//
|
||||||
|
// `await launchUrl(Uri.parse(url))` can also run if skip
|
||||||
|
// 1. The following check
|
||||||
|
// 2. `<action android:name="android.support.customtabs.action.CustomTabsService" />` in AndroidManifest.xml
|
||||||
|
//
|
||||||
|
// But it is better to add the check.
|
||||||
|
await launchUrl(Uri.parse(url));
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
@@ -158,7 +159,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
child: Autocomplete<Peer>(
|
child: Autocomplete<Peer>(
|
||||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
if (textEditingValue.text == '') {
|
if (textEditingValue.text == '') {
|
||||||
return const Iterable<Peer>.empty();
|
_autocompleteOpts = const Iterable<Peer>.empty();
|
||||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||||
Peer emptyPeer = Peer(
|
Peer emptyPeer = Peer(
|
||||||
id: '',
|
id: '',
|
||||||
@@ -174,7 +175,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
rdpUsername: '',
|
rdpUsername: '',
|
||||||
loginName: '',
|
loginName: '',
|
||||||
);
|
);
|
||||||
return [emptyPeer];
|
_autocompleteOpts = [emptyPeer];
|
||||||
} else {
|
} else {
|
||||||
String textWithoutSpaces =
|
String textWithoutSpaces =
|
||||||
textEditingValue.text.replaceAll(" ", "");
|
textEditingValue.text.replaceAll(" ", "");
|
||||||
@@ -186,7 +187,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
}
|
}
|
||||||
String textToFind = textEditingValue.text.toLowerCase();
|
String textToFind = textEditingValue.text.toLowerCase();
|
||||||
|
|
||||||
return peers
|
_autocompleteOpts = peers
|
||||||
.where((peer) =>
|
.where((peer) =>
|
||||||
peer.id.toLowerCase().contains(textToFind) ||
|
peer.id.toLowerCase().contains(textToFind) ||
|
||||||
peer.username
|
peer.username
|
||||||
@@ -198,12 +199,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
peer.alias.toLowerCase().contains(textToFind))
|
peer.alias.toLowerCase().contains(textToFind))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
return _autocompleteOpts;
|
||||||
},
|
},
|
||||||
fieldViewBuilder: (BuildContext context,
|
fieldViewBuilder: (BuildContext context,
|
||||||
TextEditingController fieldTextEditingController,
|
TextEditingController fieldTextEditingController,
|
||||||
FocusNode fieldFocusNode,
|
FocusNode fieldFocusNode,
|
||||||
VoidCallback onFieldSubmitted) {
|
VoidCallback onFieldSubmitted) {
|
||||||
fieldTextEditingController.text = _idController.text;
|
fieldTextEditingController.text = _idController.text;
|
||||||
|
Get.put<TextEditingController>(
|
||||||
|
fieldTextEditingController);
|
||||||
fieldFocusNode.addListener(() async {
|
fieldFocusNode.addListener(() async {
|
||||||
_idEmpty.value =
|
_idEmpty.value =
|
||||||
fieldTextEditingController.text.isEmpty;
|
fieldTextEditingController.text.isEmpty;
|
||||||
@@ -250,6 +254,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
inputFormatters: [IDTextInputFormatter()],
|
inputFormatters: [IDTextInputFormatter()],
|
||||||
|
onSubmitted: (_) {
|
||||||
|
onConnect();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSelected: (option) {
|
onSelected: (option) {
|
||||||
@@ -261,6 +268,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
optionsViewBuilder: (BuildContext context,
|
optionsViewBuilder: (BuildContext context,
|
||||||
AutocompleteOnSelected<Peer> onSelected,
|
AutocompleteOnSelected<Peer> onSelected,
|
||||||
Iterable<Peer> options) {
|
Iterable<Peer> options) {
|
||||||
|
options = _autocompleteOpts;
|
||||||
double maxHeight = options.length * 50;
|
double maxHeight = options.length * 50;
|
||||||
if (options.length == 1) {
|
if (options.length == 1) {
|
||||||
maxHeight = 52;
|
maxHeight = 52;
|
||||||
@@ -339,9 +347,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final child = Column(children: [
|
||||||
|
if (isWebDesktop)
|
||||||
|
getConnectionPageTitle(context, true)
|
||||||
|
.marginOnly(bottom: 10, top: 15, left: 12),
|
||||||
|
w
|
||||||
|
]);
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Container(constraints: kMobilePageConstraints, child: w));
|
child: Container(constraints: kMobilePageConstraints, child: child));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -351,76 +365,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
if (Get.isRegistered<IDTextEditingController>()) {
|
if (Get.isRegistered<IDTextEditingController>()) {
|
||||||
Get.delete<IDTextEditingController>();
|
Get.delete<IDTextEditingController>();
|
||||||
}
|
}
|
||||||
|
if (Get.isRegistered<TextEditingController>()) {
|
||||||
|
Get.delete<TextEditingController>();
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebMenu extends StatefulWidget {
|
|
||||||
const WebMenu({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<WebMenu> createState() => _WebMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WebMenuState extends State<WebMenu> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Provider.of<FfiModel>(context);
|
|
||||||
return PopupMenuButton<String>(
|
|
||||||
tooltip: "",
|
|
||||||
icon: const Icon(Icons.more_vert),
|
|
||||||
itemBuilder: (context) {
|
|
||||||
return (isIOS
|
|
||||||
? [
|
|
||||||
const PopupMenuItem(
|
|
||||||
value: "scan",
|
|
||||||
child: Icon(Icons.qr_code_scanner, color: Colors.black),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
: <PopupMenuItem<String>>[]) +
|
|
||||||
[
|
|
||||||
PopupMenuItem(
|
|
||||||
value: "server",
|
|
||||||
child: Text(translate('ID/Relay Server')),
|
|
||||||
)
|
|
||||||
] +
|
|
||||||
[
|
|
||||||
PopupMenuItem(
|
|
||||||
value: "login",
|
|
||||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
|
||||||
? translate("Login")
|
|
||||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
|
||||||
)
|
|
||||||
] +
|
|
||||||
[
|
|
||||||
PopupMenuItem(
|
|
||||||
value: "about",
|
|
||||||
child: Text(translate('About RustDesk')),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
},
|
|
||||||
onSelected: (value) {
|
|
||||||
if (value == 'server') {
|
|
||||||
showServerSettings(gFFI.dialogManager);
|
|
||||||
}
|
|
||||||
if (value == 'about') {
|
|
||||||
showAbout(gFFI.dialogManager);
|
|
||||||
}
|
|
||||||
if (value == 'login') {
|
|
||||||
if (gFFI.userModel.userName.value.isEmpty) {
|
|
||||||
loginDialog();
|
|
||||||
} else {
|
|
||||||
logOutConfirmDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (value == 'scan') {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (BuildContext context) => ScanPage(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -204,36 +204,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
} else if (v == "folder") {
|
} else if (v == "folder") {
|
||||||
final name = TextEditingController();
|
final name = TextEditingController();
|
||||||
gFFI.dialogManager
|
String? errorText;
|
||||||
.show((setState, close, context) => CustomAlertDialog(
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
title: Text(translate("Create Folder")),
|
name.addListener(() {
|
||||||
content: Column(
|
if (errorText != null) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
setState(() {
|
||||||
children: [
|
errorText = null;
|
||||||
TextFormField(
|
});
|
||||||
decoration: InputDecoration(
|
}
|
||||||
labelText: translate(
|
});
|
||||||
"Please enter the folder name"),
|
return CustomAlertDialog(
|
||||||
),
|
title: Text(translate("Create Folder")),
|
||||||
controller: name,
|
content: Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
],
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText:
|
||||||
|
translate("Please enter the folder name"),
|
||||||
|
errorText: errorText,
|
||||||
),
|
),
|
||||||
actions: [
|
controller: name,
|
||||||
dialogButton("Cancel",
|
).workaroundFreezeLinuxMint(),
|
||||||
onPressed: () => close(false),
|
],
|
||||||
isOutline: true),
|
),
|
||||||
dialogButton("OK", onPressed: () {
|
actions: [
|
||||||
if (name.value.text.isNotEmpty) {
|
dialogButton("Cancel",
|
||||||
currentFileController.createDir(
|
onPressed: () => close(false), isOutline: true),
|
||||||
PathUtil.join(
|
dialogButton("OK", onPressed: () {
|
||||||
currentDir.path,
|
if (name.value.text.isNotEmpty) {
|
||||||
name.value.text,
|
if (!PathUtil.validName(
|
||||||
currentOptions.isWindows));
|
name.value.text,
|
||||||
close();
|
currentFileController
|
||||||
}
|
.options.value.isWindows)) {
|
||||||
})
|
setState(() {
|
||||||
]));
|
errorText =
|
||||||
|
translate("Invalid folder name");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentFileController.createDir(PathUtil.join(
|
||||||
|
currentDir.path,
|
||||||
|
name.value.text,
|
||||||
|
currentOptions.isWindows));
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
});
|
||||||
} else if (v == "hidden") {
|
} else if (v == "hidden") {
|
||||||
currentFileController.toggleShowHidden();
|
currentFileController.toggleShowHidden();
|
||||||
}
|
}
|
||||||
@@ -497,7 +515,15 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
child: Text(translate("Properties")),
|
child: Text(translate("Properties")),
|
||||||
value: "properties",
|
value: "properties",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
)
|
),
|
||||||
|
if (!entries[index].isDrive &&
|
||||||
|
versionCmp(gFFI.ffiModel.pi.version,
|
||||||
|
"1.3.0") >=
|
||||||
|
0)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(translate("Rename")),
|
||||||
|
value: "rename",
|
||||||
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onSelected: (v) {
|
onSelected: (v) {
|
||||||
@@ -509,6 +535,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
_selectedItems.clear();
|
_selectedItems.clear();
|
||||||
widget.selectMode.toggle(isLocal);
|
widget.selectMode.toggle(isLocal);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
} else if (v == "rename") {
|
||||||
|
controller.renameAction(
|
||||||
|
entries[index], isLocal);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/mobile/pages/server_page.dart';
|
import 'package:flutter_hbb/mobile/pages/server_page.dart';
|
||||||
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||||
|
import 'package:flutter_hbb/web/settings_page.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/widgets/chat_page.dart';
|
import '../../common/widgets/chat_page.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
import 'connection_page.dart';
|
import 'connection_page.dart';
|
||||||
|
|
||||||
abstract class PageShape extends Widget {
|
abstract class PageShape extends Widget {
|
||||||
@@ -45,7 +47,11 @@ class HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
void initPages() {
|
void initPages() {
|
||||||
_pages.clear();
|
_pages.clear();
|
||||||
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
|
if (!bind.isIncomingOnly()) {
|
||||||
|
_pages.add(ConnectionPage(
|
||||||
|
appBarActions: [],
|
||||||
|
));
|
||||||
|
}
|
||||||
if (isAndroid && !bind.isOutgoingOnly()) {
|
if (isAndroid && !bind.isOutgoingOnly()) {
|
||||||
_chatPageTabIndex = _pages.length;
|
_chatPageTabIndex = _pages.length;
|
||||||
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
||||||
@@ -149,18 +155,80 @@ class HomePageState extends State<HomePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WebHomePage extends StatelessWidget {
|
class WebHomePage extends StatelessWidget {
|
||||||
final connectionPage = ConnectionPage();
|
final connectionPage =
|
||||||
|
ConnectionPage(appBarActions: <Widget>[const WebSettingsPage()]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
stateGlobal.isInMainPage = true;
|
||||||
|
handleUnilink(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// backgroundColor: MyTheme.grayBg,
|
// backgroundColor: MyTheme.grayBg,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text(bind.mainGetAppNameSync()),
|
title: Text("${bind.mainGetAppNameSync()} (Preview)"),
|
||||||
actions: connectionPage.appBarActions,
|
actions: connectionPage.appBarActions,
|
||||||
),
|
),
|
||||||
body: connectionPage,
|
body: connectionPage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUnilink(BuildContext context) {
|
||||||
|
if (webInitialLink.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final link = webInitialLink;
|
||||||
|
webInitialLink = '';
|
||||||
|
final splitter = ["/#/", "/#", "#/", "#"];
|
||||||
|
var fakelink = '';
|
||||||
|
for (var s in splitter) {
|
||||||
|
if (link.contains(s)) {
|
||||||
|
var list = link.split(s);
|
||||||
|
if (list.length < 2 || list[1].isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.removeAt(0);
|
||||||
|
fakelink = "rustdesk://${list.join(s)}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fakelink.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final uri = Uri.tryParse(fakelink);
|
||||||
|
if (uri == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final args = urlLinkToCmdArgs(uri);
|
||||||
|
if (args == null || args.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool isFileTransfer = false;
|
||||||
|
String? id;
|
||||||
|
String? password;
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
switch (args[i]) {
|
||||||
|
case '--connect':
|
||||||
|
case '--play':
|
||||||
|
isFileTransfer = false;
|
||||||
|
id = args[i + 1];
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case '--file-transfer':
|
||||||
|
isFileTransfer = true;
|
||||||
|
id = args[i + 1];
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case '--password':
|
||||||
|
password = args[i + 1];
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (id != null) {
|
||||||
|
connect(context, id, isFileTransfer: isFileTransfer, password: password);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -25,6 +26,19 @@ import '../widgets/dialog.dart';
|
|||||||
|
|
||||||
final initText = '1' * 1024;
|
final initText = '1' * 1024;
|
||||||
|
|
||||||
|
// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard.
|
||||||
|
// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard.
|
||||||
|
// https://github.com/flutter/flutter/issues/159384
|
||||||
|
// https://github.com/flutter/flutter/issues/159383
|
||||||
|
void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
|
||||||
|
if (isAndroid) {
|
||||||
|
if (isKeyboardVisible != true) {
|
||||||
|
// `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`.
|
||||||
|
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RemotePage extends StatefulWidget {
|
class RemotePage extends StatefulWidget {
|
||||||
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
|
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -34,15 +48,18 @@ class RemotePage extends StatefulWidget {
|
|||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RemotePage> createState() => _RemotePageState();
|
State<RemotePage> createState() => _RemotePageState(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RemotePageState extends State<RemotePage> {
|
class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
bool _showBar = !isWebDesktop;
|
bool _showBar = !isWebDesktop;
|
||||||
bool _showGestureHelp = false;
|
bool _showGestureHelp = false;
|
||||||
String _value = '';
|
String _value = '';
|
||||||
Orientation? _currentOrientation;
|
Orientation? _currentOrientation;
|
||||||
|
double _viewInsetsBottom = 0;
|
||||||
|
|
||||||
|
Timer? _timerDidChangeMetrics;
|
||||||
|
|
||||||
final _blockableOverlayState = BlockableOverlayState();
|
final _blockableOverlayState = BlockableOverlayState();
|
||||||
|
|
||||||
@@ -58,6 +75,12 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
final TextEditingController _textController =
|
final TextEditingController _textController =
|
||||||
TextEditingController(text: initText);
|
TextEditingController(text: initText);
|
||||||
|
|
||||||
|
_RemotePageState(String id) {
|
||||||
|
initSharedStates(id);
|
||||||
|
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
|
gFFI.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -80,16 +103,24 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
keyboardSubscription =
|
keyboardSubscription =
|
||||||
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
||||||
initSharedStates(widget.id);
|
|
||||||
gFFI.chatModel
|
gFFI.chatModel
|
||||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||||
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
|
||||||
_blockableOverlayState.applyFfi(gFFI);
|
_blockableOverlayState.applyFfi(gFFI);
|
||||||
gFFI.dialogManager.loadMobileActionsOverlayVisible();
|
gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
|
gFFI.recordingModel
|
||||||
|
.updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
|
||||||
|
if (gFFI.recordingModel.start) {
|
||||||
|
showToast(translate('Automatically record outgoing sessions'));
|
||||||
|
}
|
||||||
|
_disableAndroidSoftKeyboard(
|
||||||
|
isKeyboardVisible: keyboardVisibilityController.isVisible);
|
||||||
|
});
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
|
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
|
||||||
@@ -101,6 +132,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
_physicalFocusNode.dispose();
|
_physicalFocusNode.dispose();
|
||||||
await gFFI.close();
|
await gFFI.close();
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
_timerDidChangeMetrics?.cancel();
|
||||||
gFFI.dialogManager.dismissAll();
|
gFFI.dialogManager.dismissAll();
|
||||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
overlays: SystemUiOverlay.values);
|
overlays: SystemUiOverlay.values);
|
||||||
@@ -115,6 +147,39 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.chatModel.onVoiceCallClosed("End connetion");
|
gFFI.chatModel.onVoiceCallClosed("End connetion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
trySyncClipboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For client side
|
||||||
|
// When swithing from other app to this app, try to sync clipboard.
|
||||||
|
void trySyncClipboard() {
|
||||||
|
gFFI.invokeMethod("try_sync_clipboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeMetrics() {
|
||||||
|
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
|
||||||
|
// Don't try reset the view style and focus the cursor.
|
||||||
|
if (gFFI.cursorModel.lastKeyboardIsVisible &&
|
||||||
|
gFFI.canvasModel.isMobileCanvasChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
|
||||||
|
_timerDidChangeMetrics?.cancel();
|
||||||
|
_timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
|
||||||
|
// We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
|
||||||
|
if (newBottom != _viewInsetsBottom) {
|
||||||
|
gFFI.canvasModel.mobileFocusCanvasCursor();
|
||||||
|
_viewInsetsBottom = newBottom;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||||
// But for now, the transparent color will cause the canvas to be white.
|
// But for now, the transparent color will cause the canvas to be white.
|
||||||
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
||||||
@@ -152,9 +217,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
var oldValue = _value;
|
var oldValue = _value;
|
||||||
_value = newValue;
|
_value = newValue;
|
||||||
var i = newValue.length - 1;
|
var i = newValue.length - 1;
|
||||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
for (; i >= 0 && newValue[i] != '1'; --i) {}
|
||||||
var j = oldValue.length - 1;
|
var j = oldValue.length - 1;
|
||||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
for (; j >= 0 && oldValue[j] != '1'; --j) {}
|
||||||
if (i < j) j = i;
|
if (i < j) j = i;
|
||||||
var subNewValue = newValue.substring(j + 1);
|
var subNewValue = newValue.substring(j + 1);
|
||||||
var subOldValue = oldValue.substring(j + 1);
|
var subOldValue = oldValue.substring(j + 1);
|
||||||
@@ -203,8 +268,8 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
_value = newValue;
|
_value = newValue;
|
||||||
if (oldValue.isNotEmpty &&
|
if (oldValue.isNotEmpty &&
|
||||||
newValue.isNotEmpty &&
|
newValue.isNotEmpty &&
|
||||||
oldValue[0] == '\1' &&
|
oldValue[0] == '1' &&
|
||||||
newValue[0] != '\1') {
|
newValue[0] != '1') {
|
||||||
// clipboard
|
// clipboard
|
||||||
oldValue = '';
|
oldValue = '';
|
||||||
}
|
}
|
||||||
@@ -508,7 +573,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
right: 10,
|
right: 10,
|
||||||
child: QualityMonitor(gFFI.qualityMonitorModel),
|
child: QualityMonitor(gFFI.qualityMonitorModel),
|
||||||
),
|
),
|
||||||
KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)),
|
KeyHelpTools(
|
||||||
|
keyboardIsVisible: keyboardIsVisible,
|
||||||
|
showGestureHelp: _showGestureHelp),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
@@ -528,8 +595,16 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
controller: _textController,
|
controller: _textController,
|
||||||
// trick way to make backspace work always
|
// trick way to make backspace work always
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
|
// `onChanged` may be called depending on the input method if this widget is wrapped in
|
||||||
|
// `Focus(onKeyEvent: ..., child: ...)`
|
||||||
|
// For `Backspace` button in the soft keyboard:
|
||||||
|
// en/fr input method:
|
||||||
|
// 1. The button will not trigger `onKeyEvent` if the text field is not empty.
|
||||||
|
// 2. The button will trigger `onKeyEvent` if the text field is empty.
|
||||||
|
// ko/zh/ja input method: the button will trigger `onKeyEvent`
|
||||||
|
// and the event will not popup if `KeyEventResult.handled` is returned.
|
||||||
onChanged: handleSoftKeyboardInput,
|
onChanged: handleSoftKeyboardInput,
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (showCursorPaint) {
|
if (showCursorPaint) {
|
||||||
@@ -737,10 +812,14 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class KeyHelpTools extends StatefulWidget {
|
class KeyHelpTools extends StatefulWidget {
|
||||||
/// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
|
final bool keyboardIsVisible;
|
||||||
final bool requestShow;
|
final bool showGestureHelp;
|
||||||
|
|
||||||
KeyHelpTools({required this.requestShow});
|
/// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
|
||||||
|
bool get requestShow => keyboardIsVisible || showGestureHelp;
|
||||||
|
|
||||||
|
KeyHelpTools(
|
||||||
|
{required this.keyboardIsVisible, required this.showGestureHelp});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<KeyHelpTools> createState() => _KeyHelpToolsState();
|
State<KeyHelpTools> createState() => _KeyHelpToolsState();
|
||||||
@@ -776,11 +855,6 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
onPressed: onPressed);
|
onPressed: onPressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateRect() {
|
_updateRect() {
|
||||||
RenderObject? renderObject = _key.currentContext?.findRenderObject();
|
RenderObject? renderObject = _key.currentContext?.findRenderObject();
|
||||||
if (renderObject == null) {
|
if (renderObject == null) {
|
||||||
@@ -790,7 +864,8 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
final size = renderObject.size;
|
final size = renderObject.size;
|
||||||
Offset pos = renderObject.localToGlobal(Offset.zero);
|
Offset pos = renderObject.localToGlobal(Offset.zero);
|
||||||
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
|
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
|
||||||
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height));
|
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height),
|
||||||
|
widget.keyboardIsVisible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,13 +877,16 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
inputModel.command;
|
inputModel.command;
|
||||||
|
|
||||||
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
||||||
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null);
|
gFFI.cursorModel
|
||||||
|
.keyHelpToolsVisibilityChanged(null, widget.keyboardIsVisible);
|
||||||
return Offstage();
|
return Offstage();
|
||||||
}
|
}
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
|
|
||||||
final pi = gFFI.ffiModel.pi;
|
final pi = gFFI.ffiModel.pi;
|
||||||
final isMac = pi.platform == kPeerPlatformMacOS;
|
final isMac = pi.platform == kPeerPlatformMacOS;
|
||||||
|
final isWin = pi.platform == kPeerPlatformWindows;
|
||||||
|
final isLinux = pi.platform == kPeerPlatformLinux;
|
||||||
final modifiers = <Widget>[
|
final modifiers = <Widget>[
|
||||||
wrap('Ctrl ', () {
|
wrap('Ctrl ', () {
|
||||||
setState(() => inputModel.ctrl = !inputModel.ctrl);
|
setState(() => inputModel.ctrl = !inputModel.ctrl);
|
||||||
@@ -889,6 +967,28 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
wrap('PgDn', () {
|
wrap('PgDn', () {
|
||||||
inputModel.inputKey('VK_NEXT');
|
inputModel.inputKey('VK_NEXT');
|
||||||
}),
|
}),
|
||||||
|
// to-do: support PrtScr on Mac
|
||||||
|
if (isWin || isLinux)
|
||||||
|
wrap('PrtScr', () {
|
||||||
|
inputModel.inputKey('VK_SNAPSHOT');
|
||||||
|
}),
|
||||||
|
if (isWin || isLinux)
|
||||||
|
wrap('ScrollLock', () {
|
||||||
|
inputModel.inputKey('VK_SCROLL');
|
||||||
|
}),
|
||||||
|
if (isWin || isLinux)
|
||||||
|
wrap('Pause', () {
|
||||||
|
inputModel.inputKey('VK_PAUSE');
|
||||||
|
}),
|
||||||
|
if (isWin || isLinux)
|
||||||
|
// Maybe it's better to call it "Menu"
|
||||||
|
// https://en.wikipedia.org/wiki/Menu_key
|
||||||
|
wrap('Menu', () {
|
||||||
|
inputModel.inputKey('Apps');
|
||||||
|
}),
|
||||||
|
wrap('Enter', () {
|
||||||
|
inputModel.inputKey('VK_ENTER');
|
||||||
|
}),
|
||||||
SizedBox(width: 9999),
|
SizedBox(width: 9999),
|
||||||
wrap('', () {
|
wrap('', () {
|
||||||
inputModel.inputKey('VK_LEFT');
|
inputModel.inputKey('VK_LEFT');
|
||||||
@@ -939,11 +1039,11 @@ class ImagePaint extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final m = Provider.of<ImageModel>(context);
|
final m = Provider.of<ImageModel>(context);
|
||||||
final c = Provider.of<CanvasModel>(context);
|
final c = Provider.of<CanvasModel>(context);
|
||||||
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
|
||||||
var s = c.scale;
|
var s = c.scale;
|
||||||
|
final adjust = c.getAdjustY();
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: ImagePainter(
|
painter: ImagePainter(
|
||||||
image: m.image, x: c.x / s, y: (c.y - adjust) / s, scale: s),
|
image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -957,7 +1057,6 @@ class CursorPaint extends StatelessWidget {
|
|||||||
final m = Provider.of<CursorModel>(context);
|
final m = Provider.of<CursorModel>(context);
|
||||||
final c = Provider.of<CanvasModel>(context);
|
final c = Provider.of<CanvasModel>(context);
|
||||||
final ffiModel = Provider.of<FfiModel>(context);
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
|
||||||
final s = c.scale;
|
final s = c.scale;
|
||||||
double hotx = m.hotx;
|
double hotx = m.hotx;
|
||||||
double hoty = m.hoty;
|
double hoty = m.hoty;
|
||||||
@@ -989,11 +1088,12 @@ class CursorPaint extends StatelessWidget {
|
|||||||
factor = s / mins;
|
factor = s / mins;
|
||||||
}
|
}
|
||||||
final s2 = s < mins ? mins : s;
|
final s2 = s < mins ? mins : s;
|
||||||
|
final adjust = c.getAdjustY();
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: ImagePainter(
|
painter: ImagePainter(
|
||||||
image: image,
|
image: image,
|
||||||
x: (m.x - hotx) * factor + c.x / s2,
|
x: (m.x - hotx) * factor + c.x / s2,
|
||||||
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
|
y: (m.y - hoty) * factor + (c.y + adjust) / s2,
|
||||||
scale: s2),
|
scale: s2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1196,7 +1296,9 @@ void showOptions(
|
|||||||
toggles +
|
toggles +
|
||||||
[privacyModeWidget]),
|
[privacyModeWidget]),
|
||||||
);
|
);
|
||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||||
|
_disableAndroidSoftKeyboard();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
||||||
@@ -1215,7 +1317,9 @@ TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
|||||||
children: children,
|
children: children,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||||
|
_disableAndroidSoftKeyboard();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1361,9 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) {
|
|||||||
children: children,
|
children: children,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, clickMaskDismiss: true, backDismiss: true).then((value) {
|
||||||
|
_disableAndroidSoftKeyboard();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget {
|
|||||||
class _ScanPageState extends State<ScanPage> {
|
class _ScanPageState extends State<ScanPage> {
|
||||||
QRViewController? controller;
|
QRViewController? controller;
|
||||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||||
|
StreamSubscription? scanSubscription;
|
||||||
|
|
||||||
// In order to get hot reload to work we need to pause the camera if the platform
|
|
||||||
// is android, or resume the camera if the platform is iOS.
|
|
||||||
@override
|
@override
|
||||||
void reassemble() {
|
void reassemble() {
|
||||||
super.reassemble();
|
super.reassemble();
|
||||||
if (isAndroid) {
|
if (isAndroid && controller != null) {
|
||||||
controller!.pauseCamera();
|
controller!.pauseCamera();
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller!.resumeCamera();
|
||||||
}
|
}
|
||||||
controller!.resumeCamera();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Scan QR'),
|
title: const Text('Scan QR'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
_buildImagePickerButton(),
|
||||||
color: Colors.white,
|
_buildFlashToggleButton(),
|
||||||
icon: Icon(Icons.image_search),
|
_buildCameraSwitchButton(),
|
||||||
iconSize: 32.0,
|
],
|
||||||
onPressed: () async {
|
),
|
||||||
final ImagePicker picker = ImagePicker();
|
body: _buildQrView(context),
|
||||||
final XFile? file =
|
);
|
||||||
await picker.pickImage(source: ImageSource.gallery);
|
|
||||||
if (file != null) {
|
|
||||||
var image = img.decodeNamedImage(
|
|
||||||
file.path, File(file.path).readAsBytesSync())!;
|
|
||||||
|
|
||||||
LuminanceSource source = RGBLuminanceSource(
|
|
||||||
image.width,
|
|
||||||
image.height,
|
|
||||||
image
|
|
||||||
.getBytes(order: img.ChannelOrder.abgr)
|
|
||||||
.buffer
|
|
||||||
.asInt32List());
|
|
||||||
var bitmap = BinaryBitmap(HybridBinarizer(source));
|
|
||||||
|
|
||||||
var reader = QRCodeReader();
|
|
||||||
try {
|
|
||||||
var result = reader.decode(bitmap);
|
|
||||||
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
|
||||||
handleUriLink(uriString: result.text);
|
|
||||||
} else {
|
|
||||||
showServerSettingFromQr(result.text);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showToast('No QR code found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
IconButton(
|
|
||||||
color: Colors.yellow,
|
|
||||||
icon: Icon(Icons.flash_on),
|
|
||||||
iconSize: 32.0,
|
|
||||||
onPressed: () async {
|
|
||||||
await controller?.toggleFlash();
|
|
||||||
}),
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(Icons.switch_camera),
|
|
||||||
iconSize: 32.0,
|
|
||||||
onPressed: () async {
|
|
||||||
await controller?.flipCamera();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: _buildQrView(context));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQrView(BuildContext context) {
|
Widget _buildQrView(BuildContext context) {
|
||||||
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
|
var scanArea = MediaQuery.of(context).size.width < 400 ||
|
||||||
var scanArea = (MediaQuery.of(context).size.width < 400 ||
|
MediaQuery.of(context).size.height < 400
|
||||||
MediaQuery.of(context).size.height < 400)
|
|
||||||
? 150.0
|
? 150.0
|
||||||
: 300.0;
|
: 300.0;
|
||||||
// To ensure the Scanner view is properly sizes after rotation
|
|
||||||
// we need to listen for Flutter SizeChanged notification and update controller
|
|
||||||
return QRView(
|
return QRView(
|
||||||
key: qrKey,
|
key: qrKey,
|
||||||
onQRViewCreated: _onQRViewCreated,
|
onQRViewCreated: _onQRViewCreated,
|
||||||
overlay: QrScannerOverlayShape(
|
overlay: QrScannerOverlayShape(
|
||||||
borderColor: Colors.red,
|
borderColor: Colors.red,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
borderLength: 30,
|
borderLength: 30,
|
||||||
borderWidth: 10,
|
borderWidth: 10,
|
||||||
cutOutSize: scanArea),
|
cutOutSize: scanArea,
|
||||||
|
),
|
||||||
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
|
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -116,7 +69,7 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
});
|
});
|
||||||
controller.scannedDataStream.listen((scanData) {
|
scanSubscription = controller.scannedDataStream.listen((scanData) {
|
||||||
if (scanData.code != null) {
|
if (scanData.code != null) {
|
||||||
showServerSettingFromQr(scanData.code!);
|
showServerSettingFromQr(scanData.code!);
|
||||||
}
|
}
|
||||||
@@ -129,8 +82,66 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pickImage() async {
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
final XFile? file = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
if (file != null) {
|
||||||
|
try {
|
||||||
|
var image = img.decodeImage(await File(file.path).readAsBytes())!;
|
||||||
|
LuminanceSource source = RGBLuminanceSource(
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(),
|
||||||
|
);
|
||||||
|
var bitmap = BinaryBitmap(HybridBinarizer(source));
|
||||||
|
|
||||||
|
var reader = QRCodeReader();
|
||||||
|
var result = reader.decode(bitmap);
|
||||||
|
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
||||||
|
handleUriLink(uriString: result.text);
|
||||||
|
} else {
|
||||||
|
showServerSettingFromQr(result.text);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('No QR code found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagePickerButton() {
|
||||||
|
return IconButton(
|
||||||
|
color: Colors.white,
|
||||||
|
icon: Icon(Icons.image_search),
|
||||||
|
iconSize: 32.0,
|
||||||
|
onPressed: _pickImage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFlashToggleButton() {
|
||||||
|
return IconButton(
|
||||||
|
color: Colors.yellow,
|
||||||
|
icon: Icon(Icons.flash_on),
|
||||||
|
iconSize: 32.0,
|
||||||
|
onPressed: () async {
|
||||||
|
await controller?.toggleFlash();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCameraSwitchButton() {
|
||||||
|
return IconButton(
|
||||||
|
color: Colors.white,
|
||||||
|
icon: Icon(Icons.switch_camera),
|
||||||
|
iconSize: 32.0,
|
||||||
|
onPressed: () async {
|
||||||
|
await controller?.flipCamera();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
scanSubscription?.cancel();
|
||||||
controller?.dispose();
|
controller?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class _ServerPageState extends State<ServerPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
buildPresetPasswordWarning(),
|
buildPresetPasswordWarningMobile(),
|
||||||
gFFI.serverModel.isStart
|
gFFI.serverModel.isStart
|
||||||
? ServerInfo()
|
? ServerInfo()
|
||||||
: ServiceNotRunningNotification(),
|
: ServiceNotRunningNotification(),
|
||||||
@@ -446,7 +446,6 @@ class ServerInfo extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isPermanent = model.verificationMethod == kUsePermanentPassword;
|
|
||||||
final serverModel = Provider.of<ServerModel>(context);
|
final serverModel = Provider.of<ServerModel>(context);
|
||||||
|
|
||||||
const Color colorPositive = Colors.green;
|
const Color colorPositive = Colors.green;
|
||||||
@@ -486,6 +485,8 @@ class ServerInfo extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final showOneTime = serverModel.approveMode != 'click' &&
|
||||||
|
serverModel.verificationMethod != kUsePermanentPassword;
|
||||||
return PaddingCard(
|
return PaddingCard(
|
||||||
title: translate('Your Device'),
|
title: translate('Your Device'),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -523,10 +524,10 @@ class ServerInfo extends StatelessWidget {
|
|||||||
]),
|
]),
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
Text(
|
Text(
|
||||||
isPermanent ? '-' : model.serverPasswd.value.text,
|
!showOneTime ? '-' : model.serverPasswd.value.text,
|
||||||
style: textStyleValue,
|
style: textStyleValue,
|
||||||
),
|
),
|
||||||
isPermanent
|
!showOneTime
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: Row(children: [
|
: Row(children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -595,7 +596,9 @@ class _PermissionCheckerState extends State<PermissionChecker> {
|
|||||||
translate("android_version_audio_tip"),
|
translate("android_version_audio_tip"),
|
||||||
style: const TextStyle(color: MyTheme.darkGray),
|
style: const TextStyle(color: MyTheme.darkGray),
|
||||||
))
|
))
|
||||||
])
|
]),
|
||||||
|
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
|
||||||
|
serverModel.toggleClipboard),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
import 'package:settings_ui/settings_ui.dart';
|
||||||
@@ -69,6 +71,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
false; //androidVersion >= 26; // remove because not work on every device
|
false; //androidVersion >= 26; // remove because not work on every device
|
||||||
var _ignoreBatteryOpt = false;
|
var _ignoreBatteryOpt = false;
|
||||||
var _enableStartOnBoot = false;
|
var _enableStartOnBoot = false;
|
||||||
|
var _checkUpdateOnStartup = false;
|
||||||
var _floatingWindowDisabled = false;
|
var _floatingWindowDisabled = false;
|
||||||
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
|
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
|
||||||
var _enableAbr = false;
|
var _enableAbr = false;
|
||||||
@@ -78,6 +81,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _enableRecordSession = false;
|
var _enableRecordSession = false;
|
||||||
var _enableHardwareCodec = false;
|
var _enableHardwareCodec = false;
|
||||||
var _autoRecordIncomingSession = false;
|
var _autoRecordIncomingSession = false;
|
||||||
|
var _autoRecordOutgoingSession = false;
|
||||||
var _allowAutoDisconnect = false;
|
var _allowAutoDisconnect = false;
|
||||||
var _localIP = "";
|
var _localIP = "";
|
||||||
var _directAccessPort = "";
|
var _directAccessPort = "";
|
||||||
@@ -87,12 +91,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _hideServer = false;
|
var _hideServer = false;
|
||||||
var _hideProxy = false;
|
var _hideProxy = false;
|
||||||
var _hideNetwork = false;
|
var _hideNetwork = false;
|
||||||
|
var _enableTrustedDevices = false;
|
||||||
|
|
||||||
@override
|
_SettingsState() {
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
|
|
||||||
_enableAbr = option2bool(
|
_enableAbr = option2bool(
|
||||||
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
||||||
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
||||||
@@ -106,6 +107,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||||
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||||
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||||
|
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
|
||||||
|
bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing));
|
||||||
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
||||||
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
||||||
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||||
@@ -117,8 +120,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||||
_hideNetwork =
|
_hideNetwork =
|
||||||
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
||||||
|
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
||||||
|
}
|
||||||
|
|
||||||
() async {
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
var update = false;
|
var update = false;
|
||||||
|
|
||||||
if (_hasIgnoreBattery) {
|
if (_hasIgnoreBattery) {
|
||||||
@@ -146,6 +156,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_enableStartOnBoot = enableStartOnBoot;
|
_enableStartOnBoot = enableStartOnBoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var checkUpdateOnStartup =
|
||||||
|
mainGetLocalBoolOptionSync(kOptionEnableCheckUpdate);
|
||||||
|
if (checkUpdateOnStartup != _checkUpdateOnStartup) {
|
||||||
|
update = true;
|
||||||
|
_checkUpdateOnStartup = checkUpdateOnStartup;
|
||||||
|
}
|
||||||
|
|
||||||
var floatingWindowDisabled =
|
var floatingWindowDisabled =
|
||||||
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
||||||
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
||||||
@@ -177,7 +194,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
if (update) {
|
if (update) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -226,6 +243,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Provider.of<FfiModel>(context);
|
Provider.of<FfiModel>(context);
|
||||||
final outgoingOnly = bind.isOutgoingOnly();
|
final outgoingOnly = bind.isOutgoingOnly();
|
||||||
|
final incommingOnly = bind.isIncomingOnly();
|
||||||
final customClientSection = CustomSettingsSection(
|
final customClientSection = CustomSettingsSection(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -241,18 +259,76 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
],
|
],
|
||||||
));
|
));
|
||||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
final enable2fa = bind.mainHasValid2FaSync();
|
||||||
|
final List<AbstractSettingsTile> tfaTiles = [
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('enable-2fa-title')),
|
title: Text(translate('enable-2fa-title')),
|
||||||
initialValue: bind.mainHasValid2FaSync(),
|
initialValue: enable2fa,
|
||||||
onToggle: (_) async {
|
onToggle: (v) async {
|
||||||
update() async {
|
update() async {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
change2fa(callback: update);
|
if (v == false) {
|
||||||
|
CommonConfirmDialog(
|
||||||
|
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
||||||
|
change2fa(callback: update);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
change2fa(callback: update);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (enable2fa)
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title: Text(translate('Telegram bot')),
|
||||||
|
initialValue: bind.mainHasValidBotSync(),
|
||||||
|
onToggle: (v) async {
|
||||||
|
update() async {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v == false) {
|
||||||
|
CommonConfirmDialog(
|
||||||
|
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
|
||||||
|
changeBot(callback: update);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
changeBot(callback: update);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (enable2fa)
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(translate('Enable trusted devices')),
|
||||||
|
Text('* ${translate('enable-trusted-devices-tip')}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
initialValue: _enableTrustedDevices,
|
||||||
|
onToggle: isOptionFixed(kOptionEnableTrustedDevices)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
|
mainSetBoolOption(kOptionEnableTrustedDevices, v);
|
||||||
|
setState(() {
|
||||||
|
_enableTrustedDevices = v;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (enable2fa && _enableTrustedDevices)
|
||||||
|
SettingsTile(
|
||||||
|
title: Text(translate('Manage trusted devices')),
|
||||||
|
trailing: Icon(Icons.arrow_forward_ios),
|
||||||
|
onPressed: (context) {
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||||
|
return _ManageTrustedDevices();
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
];
|
||||||
|
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Deny LAN discovery')),
|
title: Text(translate('Deny LAN discovery')),
|
||||||
initialValue: _denyLANDiscovery,
|
initialValue: _denyLANDiscovery,
|
||||||
@@ -485,6 +561,22 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (!bind.isCustomClient()) {
|
||||||
|
enhancementsTiles.add(
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
initialValue: _checkUpdateOnStartup,
|
||||||
|
title:
|
||||||
|
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(translate('Check for software update on startup')),
|
||||||
|
]),
|
||||||
|
onToggle: (bool toValue) async {
|
||||||
|
await mainSetLocalBoolOption(kOptionEnableCheckUpdate, toValue);
|
||||||
|
setState(() => _checkUpdateOnStartup = toValue);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onFloatingWindowChanged(bool toValue) async {
|
onFloatingWindowChanged(bool toValue) async {
|
||||||
if (toValue) {
|
if (toValue) {
|
||||||
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||||
@@ -611,35 +703,63 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
if (isAndroid && !outgoingOnly)
|
if (isAndroid)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Recording")),
|
title: Text(translate("Recording")),
|
||||||
tiles: [
|
tiles: [
|
||||||
SettingsTile.switchTile(
|
if (!outgoingOnly)
|
||||||
title:
|
SettingsTile.switchTile(
|
||||||
Text(translate('Automatically record incoming sessions')),
|
title:
|
||||||
leading: Icon(Icons.videocam),
|
Text(translate('Automatically record incoming sessions')),
|
||||||
description: Text(
|
initialValue: _autoRecordIncomingSession,
|
||||||
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
|
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||||
initialValue: _autoRecordIncomingSession,
|
? null
|
||||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
: (v) async {
|
||||||
? null
|
await bind.mainSetOption(
|
||||||
: (v) async {
|
key: kOptionAllowAutoRecordIncoming,
|
||||||
await bind.mainSetOption(
|
value: bool2option(
|
||||||
key: kOptionAllowAutoRecordIncoming,
|
kOptionAllowAutoRecordIncoming, v));
|
||||||
value:
|
final newValue = option2bool(
|
||||||
bool2option(kOptionAllowAutoRecordIncoming, v));
|
kOptionAllowAutoRecordIncoming,
|
||||||
final newValue = option2bool(
|
await bind.mainGetOption(
|
||||||
kOptionAllowAutoRecordIncoming,
|
key: kOptionAllowAutoRecordIncoming));
|
||||||
await bind.mainGetOption(
|
setState(() {
|
||||||
key: kOptionAllowAutoRecordIncoming));
|
_autoRecordIncomingSession = newValue;
|
||||||
setState(() {
|
});
|
||||||
_autoRecordIncomingSession = newValue;
|
},
|
||||||
});
|
),
|
||||||
},
|
if (!incommingOnly)
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title:
|
||||||
|
Text(translate('Automatically record outgoing sessions')),
|
||||||
|
initialValue: _autoRecordOutgoingSession,
|
||||||
|
onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
|
await bind.mainSetLocalOption(
|
||||||
|
key: kOptionAllowAutoRecordOutgoing,
|
||||||
|
value: bool2option(
|
||||||
|
kOptionAllowAutoRecordOutgoing, v));
|
||||||
|
final newValue = option2bool(
|
||||||
|
kOptionAllowAutoRecordOutgoing,
|
||||||
|
bind.mainGetLocalOption(
|
||||||
|
key: kOptionAllowAutoRecordOutgoing));
|
||||||
|
setState(() {
|
||||||
|
_autoRecordOutgoingSession = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SettingsTile(
|
||||||
|
title: Text(translate("Directory")),
|
||||||
|
description: Text(bind.mainVideoSaveDirectory(root: false)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (isAndroid &&
|
||||||
|
!disabledSettings &&
|
||||||
|
!outgoingOnly &&
|
||||||
|
!hideSecuritySettings)
|
||||||
|
SettingsSection(title: Text('2FA'), tiles: tfaTiles),
|
||||||
if (isAndroid &&
|
if (isAndroid &&
|
||||||
!disabledSettings &&
|
!disabledSettings &&
|
||||||
!outgoingOnly &&
|
!outgoingOnly &&
|
||||||
@@ -662,9 +782,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
tiles: [
|
tiles: [
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
if (await canLaunchUrl(Uri.parse(url))) {
|
await launchUrl(Uri.parse(url));
|
||||||
await launchUrl(Uri.parse(url));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
title: Text(translate("Version: ") + version),
|
title: Text(translate("Version: ") + version),
|
||||||
value: Padding(
|
value: Padding(
|
||||||
@@ -733,11 +851,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
|
||||||
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
|
|
||||||
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||||
try {
|
try {
|
||||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
||||||
@@ -813,9 +926,7 @@ void showAbout(OverlayDialogManager dialogManager) {
|
|||||||
InkWell(
|
InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
const url = 'https://rustdesk.com/';
|
const url = 'https://rustdesk.com/';
|
||||||
if (await canLaunchUrl(Uri.parse(url))) {
|
await launchUrl(Uri.parse(url));
|
||||||
await launchUrl(Uri.parse(url));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
@@ -961,6 +1072,51 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ManageTrustedDevices extends StatefulWidget {
|
||||||
|
const _ManageTrustedDevices();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> {
|
||||||
|
RxList<TrustedDevice> trustedDevices = RxList.empty(growable: true);
|
||||||
|
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(translate('Manage trusted devices')),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
Obx(() => IconButton(
|
||||||
|
icon: Icon(Icons.delete, color: Colors.white),
|
||||||
|
onPressed: selectedDevices.isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
confrimDeleteTrustedDevicesDialog(
|
||||||
|
trustedDevices, selectedDevices);
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: FutureBuilder(
|
||||||
|
future: TrustedDevice.get(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('Error: ${snapshot.error}'));
|
||||||
|
}
|
||||||
|
final devices = snapshot.data as List<TrustedDevice>;
|
||||||
|
trustedDevices = devices.obs;
|
||||||
|
return trustedDevicesTable(trustedDevices, selectedDevices);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _RadioEntry {
|
class _RadioEntry {
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||||
@@ -65,7 +66,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
|||||||
? null
|
? null
|
||||||
: translate('Too short, at least 6 characters.');
|
: translate('Too short, at least 6 characters.');
|
||||||
},
|
},
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
keyboardType: TextInputType.visiblePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
@@ -84,7 +85,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
|||||||
? null
|
? null
|
||||||
: translate('The confirmation is not identical.');
|
: translate('The confirmation is not identical.');
|
||||||
},
|
},
|
||||||
),
|
).workaroundFreezeLinuxMint(),
|
||||||
])),
|
])),
|
||||||
onCancel: close,
|
onCancel: close,
|
||||||
onSubmit: (validateLength && validateSame) ? submit : null,
|
onSubmit: (validateLength && validateSame) ? submit : null,
|
||||||
@@ -146,6 +147,16 @@ void setTemporaryPasswordLengthDialog(
|
|||||||
}, backDismiss: true, clickMaskDismiss: true);
|
}, backDismiss: true, clickMaskDismiss: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||||
|
Map<String, dynamic> options = {};
|
||||||
|
try {
|
||||||
|
options = jsonDecode(await bind.mainGetOptions());
|
||||||
|
} catch (e) {
|
||||||
|
print("Invalid server config: $e");
|
||||||
|
}
|
||||||
|
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
|
||||||
|
}
|
||||||
|
|
||||||
void showServerSettingsWithValue(
|
void showServerSettingsWithValue(
|
||||||
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
|
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
@@ -184,6 +195,43 @@ void showServerSettingsWithValue(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildField(
|
||||||
|
String label, TextEditingController controller, String errorMsg,
|
||||||
|
{String? Function(String?)? validator, bool autofocus = false}) {
|
||||||
|
if (isDesktop || isWeb) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: Text(label),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
errorText: errorMsg.isEmpty ? null : errorMsg,
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
|
),
|
||||||
|
validator: validator,
|
||||||
|
autofocus: autofocus,
|
||||||
|
).workaroundFreezeLinuxMint(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
errorText: errorMsg.isEmpty ? null : errorMsg,
|
||||||
|
),
|
||||||
|
validator: validator,
|
||||||
|
).workaroundFreezeLinuxMint();
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -191,55 +239,45 @@ void showServerSettingsWithValue(
|
|||||||
...ServerConfigImportExportWidgets(controllers, errMsgs),
|
...ServerConfigImportExportWidgets(controllers, errMsgs),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: Form(
|
content: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 500),
|
||||||
|
child: Form(
|
||||||
child: Obx(() => Column(
|
child: Obx(() => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: [
|
||||||
TextFormField(
|
buildField(translate('ID Server'), idCtrl, idServerMsg.value,
|
||||||
controller: idCtrl,
|
autofocus: true),
|
||||||
decoration: InputDecoration(
|
SizedBox(height: 8),
|
||||||
labelText: translate('ID Server'),
|
if (!isIOS && !isWeb) ...[
|
||||||
errorText: idServerMsg.value.isEmpty
|
buildField(translate('Relay Server'), relayCtrl,
|
||||||
? null
|
relayServerMsg.value),
|
||||||
: idServerMsg.value),
|
SizedBox(height: 8),
|
||||||
)
|
],
|
||||||
] +
|
buildField(
|
||||||
[
|
translate('API Server'),
|
||||||
TextFormField(
|
apiCtrl,
|
||||||
controller: relayCtrl,
|
apiServerMsg.value,
|
||||||
decoration: InputDecoration(
|
validator: (v) {
|
||||||
labelText: translate('Relay Server'),
|
if (v != null && v.isNotEmpty) {
|
||||||
errorText: relayServerMsg.value.isEmpty
|
if (!(v.startsWith('http://') ||
|
||||||
? null
|
v.startsWith("https://"))) {
|
||||||
: relayServerMsg.value),
|
return translate("invalid_http");
|
||||||
)
|
|
||||||
] +
|
|
||||||
[
|
|
||||||
TextFormField(
|
|
||||||
controller: apiCtrl,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: translate('API Server'),
|
|
||||||
),
|
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
||||||
validator: (v) {
|
|
||||||
if (v != null && v.isNotEmpty) {
|
|
||||||
if (!(v.startsWith('http://') ||
|
|
||||||
v.startsWith("https://"))) {
|
|
||||||
return translate("invalid_http");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
},
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
buildField('Key', keyCtrl, ''),
|
||||||
|
if (isInProgress)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
|
child: LinearProgressIndicator(),
|
||||||
),
|
),
|
||||||
TextFormField(
|
],
|
||||||
controller: keyCtrl,
|
)),
|
||||||
decoration: InputDecoration(
|
),
|
||||||
labelText: 'Key',
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
// NOT use Offstage to wrap LinearProgressIndicator
|
|
||||||
if (isInProgress) const LinearProgressIndicator(),
|
|
||||||
]))),
|
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton('Cancel', onPressed: () {
|
dialogButton('Cancel', onPressed: () {
|
||||||
close();
|
close();
|
||||||
|
|||||||
@@ -41,18 +41,16 @@ class GestureHelp extends StatefulWidget {
|
|||||||
final OnTouchModeChange onTouchModeChange;
|
final OnTouchModeChange onTouchModeChange;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _GestureHelpState();
|
State<StatefulWidget> createState() => _GestureHelpState(touchMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GestureHelpState extends State<GestureHelp> {
|
class _GestureHelpState extends State<GestureHelp> {
|
||||||
var _selectedIndex;
|
late int _selectedIndex;
|
||||||
var _touchMode;
|
late bool _touchMode;
|
||||||
|
|
||||||
@override
|
_GestureHelpState(bool touchMode) {
|
||||||
void initState() {
|
_touchMode = touchMode;
|
||||||
_touchMode = widget.touchMode;
|
|
||||||
_selectedIndex = _touchMode ? 1 : 0;
|
_selectedIndex = _touchMode ? 1 : 0;
|
||||||
super.initState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -17,22 +17,24 @@ import '../common.dart';
|
|||||||
|
|
||||||
final syncAbOption = 'sync-ab-with-recent-sessions';
|
final syncAbOption = 'sync-ab-with-recent-sessions';
|
||||||
bool shouldSyncAb() {
|
bool shouldSyncAb() {
|
||||||
return bind.mainGetLocalOption(key: syncAbOption).isNotEmpty;
|
return bind.mainGetLocalOption(key: syncAbOption) == 'Y';
|
||||||
}
|
}
|
||||||
|
|
||||||
final sortAbTagsOption = 'sync-ab-tags';
|
final sortAbTagsOption = 'sync-ab-tags';
|
||||||
bool shouldSortTags() {
|
bool shouldSortTags() {
|
||||||
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
|
return bind.mainGetLocalOption(key: sortAbTagsOption) == 'Y';
|
||||||
}
|
}
|
||||||
|
|
||||||
final filterAbTagOption = 'filter-ab-by-intersection';
|
final filterAbTagOption = 'filter-ab-by-intersection';
|
||||||
bool filterAbTagByIntersection() {
|
bool filterAbTagByIntersection() {
|
||||||
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty;
|
return bind.mainGetLocalOption(key: filterAbTagOption) == 'Y';
|
||||||
}
|
}
|
||||||
|
|
||||||
const _personalAddressBookName = "My address book";
|
const _personalAddressBookName = "My address book";
|
||||||
const _legacyAddressBookName = "Legacy address book";
|
const _legacyAddressBookName = "Legacy address book";
|
||||||
|
|
||||||
|
const kUntagged = "Untagged";
|
||||||
|
|
||||||
enum ForcePullAb {
|
enum ForcePullAb {
|
||||||
listAndCurrent,
|
listAndCurrent,
|
||||||
current,
|
current,
|
||||||
@@ -66,10 +68,16 @@ class AbModel {
|
|||||||
var listInitialized = false;
|
var listInitialized = false;
|
||||||
var _maxPeerOneAb = 0;
|
var _maxPeerOneAb = 0;
|
||||||
|
|
||||||
|
late final Peers peersModel;
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
AbModel(this.parent) {
|
AbModel(this.parent) {
|
||||||
addressbooks.clear();
|
addressbooks.clear();
|
||||||
|
peersModel = Peers(
|
||||||
|
name: PeersModelName.addressBook,
|
||||||
|
getInitPeers: () => currentAbPeers,
|
||||||
|
loadEvent: LoadEvent.addressBook);
|
||||||
if (desktopType == DesktopType.main) {
|
if (desktopType == DesktopType.main) {
|
||||||
Timer.periodic(Duration(milliseconds: 500), (timer) async {
|
Timer.periodic(Duration(milliseconds: 500), (timer) async {
|
||||||
if (_timerCounter++ % 6 == 0) {
|
if (_timerCounter++ % 6 == 0) {
|
||||||
@@ -111,9 +119,10 @@ class AbModel {
|
|||||||
Future<void> _pullAb(
|
Future<void> _pullAb(
|
||||||
{required ForcePullAb? force, required bool quiet}) async {
|
{required ForcePullAb? force, required bool quiet}) async {
|
||||||
if (bind.isDisableAb()) return;
|
if (bind.isDisableAb()) return;
|
||||||
debugPrint("pullAb, force: $force, quiet: $quiet");
|
|
||||||
if (!gFFI.userModel.isLogin) return;
|
if (!gFFI.userModel.isLogin) return;
|
||||||
|
if (gFFI.userModel.networkError.isNotEmpty) return;
|
||||||
if (force == null && listInitialized && current.initialized) return;
|
if (force == null && listInitialized && current.initialized) return;
|
||||||
|
debugPrint("pullAb, force: $force, quiet: $quiet");
|
||||||
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
|
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
|
||||||
try {
|
try {
|
||||||
// Read personal guid every time to avoid upgrading the server without closing the main window
|
// Read personal guid every time to avoid upgrading the server without closing the main window
|
||||||
@@ -417,6 +426,7 @@ class AbModel {
|
|||||||
|
|
||||||
// #region tags
|
// #region tags
|
||||||
Future<bool> addTags(List<String> tagList) async {
|
Future<bool> addTags(List<String> tagList) async {
|
||||||
|
tagList.removeWhere((e) => e == kUntagged);
|
||||||
final ret = await current.addTags(tagList, {});
|
final ret = await current.addTags(tagList, {});
|
||||||
await pullNonLegacyAfterChange();
|
await pullNonLegacyAfterChange();
|
||||||
_saveCache();
|
_saveCache();
|
||||||
@@ -638,6 +648,9 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color getCurrentAbTagColor(String tag) {
|
Color getCurrentAbTagColor(String tag) {
|
||||||
|
if (tag == kUntagged) {
|
||||||
|
return MyTheme.accent;
|
||||||
|
}
|
||||||
int? colorValue = current.tagColors[tag];
|
int? colorValue = current.tagColors[tag];
|
||||||
if (colorValue != null) {
|
if (colorValue != null) {
|
||||||
return Color(colorValue);
|
return Color(colorValue);
|
||||||
@@ -815,8 +828,6 @@ abstract class BaseAb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LegacyAb extends BaseAb {
|
class LegacyAb extends BaseAb {
|
||||||
final sortTags = shouldSortTags().obs;
|
|
||||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
|
||||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||||
// licensedDevices is obtained from personal ab, shared ab restrict it in server
|
// licensedDevices is obtained from personal ab, shared ab restrict it in server
|
||||||
var licensedDevices = 0;
|
var licensedDevices = 0;
|
||||||
@@ -1209,8 +1220,6 @@ class LegacyAb extends BaseAb {
|
|||||||
class Ab extends BaseAb {
|
class Ab extends BaseAb {
|
||||||
AbProfile profile;
|
AbProfile profile;
|
||||||
late final bool personal;
|
late final bool personal;
|
||||||
final sortTags = shouldSortTags().obs;
|
|
||||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
|
||||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||||
|
|
||||||
Ab(this.profile, this.personal);
|
Ab(this.profile, this.personal);
|
||||||
|
|||||||
@@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) ||
|
_isChatOverlayHide() =>
|
||||||
chatWindowOverlayEntry == null);
|
((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) ||
|
||||||
|
chatWindowOverlayEntry == null);
|
||||||
|
|
||||||
toggleChatOverlay({Offset? chatInitPos}) {
|
toggleChatOverlay({Offset? chatInitPos}) {
|
||||||
if (_isChatOverlayHide()) {
|
if (_isChatOverlayHide()) {
|
||||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
if (!isDesktop) {
|
if (!(isDesktop || isWebDesktop)) {
|
||||||
showChatIconOverlay();
|
showChatIconOverlay();
|
||||||
}
|
}
|
||||||
showChatWindowOverlay(chatInitPos: chatInitPos);
|
showChatWindowOverlay(chatInitPos: chatInitPos);
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class CmFileModel {
|
|||||||
_onFileRemove(evt['remove']);
|
_onFileRemove(evt['remove']);
|
||||||
} else if (evt['create_dir'] != null) {
|
} else if (evt['create_dir'] != null) {
|
||||||
_onDirCreate(evt['create_dir']);
|
_onDirCreate(evt['create_dir']);
|
||||||
|
} else if (evt['rename'] != null) {
|
||||||
|
_onRename(evt['rename']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +61,6 @@ class CmFileModel {
|
|||||||
|
|
||||||
_dealOneJob(dynamic l, bool calcSpeed) {
|
_dealOneJob(dynamic l, bool calcSpeed) {
|
||||||
final data = TransferJobSerdeData.fromJson(l);
|
final data = TransferJobSerdeData.fromJson(l);
|
||||||
Client? client =
|
|
||||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
|
||||||
var jobTable = _jobTables[data.connId];
|
var jobTable = _jobTables[data.connId];
|
||||||
if (jobTable == null) {
|
if (jobTable == null) {
|
||||||
debugPrint("jobTable should not be null");
|
debugPrint("jobTable should not be null");
|
||||||
@@ -70,12 +70,7 @@ class CmFileModel {
|
|||||||
if (job == null) {
|
if (job == null) {
|
||||||
job = CmFileLog();
|
job = CmFileLog();
|
||||||
jobTable.add(job);
|
jobTable.add(job);
|
||||||
final currentSelectedTab =
|
_addUnread(data.connId);
|
||||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
|
||||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
|
||||||
currentSelectedTab.key == data.connId.toString())) {
|
|
||||||
client?.unreadChatMessageCount.value += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
job.id = data.id;
|
job.id = data.id;
|
||||||
job.action =
|
job.action =
|
||||||
@@ -167,8 +162,6 @@ class CmFileModel {
|
|||||||
try {
|
try {
|
||||||
dynamic d = jsonDecode(log);
|
dynamic d = jsonDecode(log);
|
||||||
FileActionLog data = FileActionLog.fromJson(d);
|
FileActionLog data = FileActionLog.fromJson(d);
|
||||||
Client? client =
|
|
||||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
|
||||||
var jobTable = _jobTables[data.connId];
|
var jobTable = _jobTables[data.connId];
|
||||||
if (jobTable == null) {
|
if (jobTable == null) {
|
||||||
debugPrint("jobTable should not be null");
|
debugPrint("jobTable should not be null");
|
||||||
@@ -179,17 +172,45 @@ class CmFileModel {
|
|||||||
..fileName = data.path
|
..fileName = data.path
|
||||||
..action = CmFileAction.createDir
|
..action = CmFileAction.createDir
|
||||||
..state = JobState.done);
|
..state = JobState.done);
|
||||||
final currentSelectedTab =
|
_addUnread(data.connId);
|
||||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
|
||||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
|
||||||
currentSelectedTab.key == data.connId.toString())) {
|
|
||||||
client?.unreadChatMessageCount.value += 1;
|
|
||||||
}
|
|
||||||
jobTable.refresh();
|
jobTable.refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('$e');
|
debugPrint('$e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onRename(dynamic log) {
|
||||||
|
try {
|
||||||
|
dynamic d = jsonDecode(log);
|
||||||
|
FileRenamenLog data = FileRenamenLog.fromJson(d);
|
||||||
|
var jobTable = _jobTables[data.connId];
|
||||||
|
if (jobTable == null) {
|
||||||
|
debugPrint("jobTable should not be null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final fileName = '${data.path} -> ${data.newName}';
|
||||||
|
jobTable.add(CmFileLog()
|
||||||
|
..id = 0
|
||||||
|
..fileName = fileName
|
||||||
|
..action = CmFileAction.rename
|
||||||
|
..state = JobState.done);
|
||||||
|
_addUnread(data.connId);
|
||||||
|
jobTable.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_addUnread(int connId) {
|
||||||
|
Client? client =
|
||||||
|
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId);
|
||||||
|
final currentSelectedTab =
|
||||||
|
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||||
|
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||||
|
currentSelectedTab.key == connId.toString())) {
|
||||||
|
client?.unreadChatMessageCount.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CmFileAction {
|
enum CmFileAction {
|
||||||
@@ -198,6 +219,7 @@ enum CmFileAction {
|
|||||||
localToRemote,
|
localToRemote,
|
||||||
remove,
|
remove,
|
||||||
createDir,
|
createDir,
|
||||||
|
rename,
|
||||||
}
|
}
|
||||||
|
|
||||||
class CmFileLog {
|
class CmFileLog {
|
||||||
@@ -285,3 +307,22 @@ class FileActionLog {
|
|||||||
dir: d['dir'] ?? false,
|
dir: d['dir'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FileRenamenLog {
|
||||||
|
int connId = 0;
|
||||||
|
String path = '';
|
||||||
|
String newName = '';
|
||||||
|
|
||||||
|
FileRenamenLog({
|
||||||
|
required this.connId,
|
||||||
|
required this.path,
|
||||||
|
required this.newName,
|
||||||
|
});
|
||||||
|
|
||||||
|
FileRenamenLog.fromJson(dynamic d)
|
||||||
|
: this(
|
||||||
|
connId: d['connId'] ?? 0,
|
||||||
|
path: d['path'] ?? '',
|
||||||
|
newName: d['newName'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ class TextureModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentDisplay(int curDisplay) {
|
updateCurrentDisplay(int curDisplay) {
|
||||||
|
if (isWeb) return;
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
tryCreateTexture(int idx) {
|
tryCreateTexture(int idx) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user