mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-19 15:19:09 +08:00
Compare commits
538 Commits
1.2.3-1
...
fdroid-ver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9827c76514 | ||
|
|
712bfbae92 | ||
|
|
0152e937ec | ||
|
|
c066dc8c24 | ||
|
|
0b96b3f345 | ||
|
|
b5781933b6 | ||
|
|
d851bf8b69 | ||
|
|
b6d6a4360f | ||
|
|
69b11e8dc6 | ||
|
|
6e78037770 | ||
|
|
bed0375f45 | ||
|
|
42d723f82f | ||
|
|
fb0ce8c974 | ||
|
|
d55770f12b | ||
|
|
125c275623 | ||
|
|
c640f7ed12 | ||
|
|
73662ed7d9 | ||
|
|
a595d83232 | ||
|
|
6743b4f290 | ||
|
|
e8ecb738d0 | ||
|
|
9c20968e05 | ||
|
|
9f80690d8a | ||
|
|
e58a95e7d9 | ||
|
|
d9b96d2f31 | ||
|
|
bbe9017318 | ||
|
|
a31a68ba17 | ||
|
|
9d56e29a09 | ||
|
|
32fca6f9b3 | ||
|
|
72c2de575a | ||
|
|
93d4987dcc | ||
|
|
2a25034039 | ||
|
|
5ba1c2587d | ||
|
|
1b0a3e610e | ||
|
|
2820f3f798 | ||
|
|
09f3850250 | ||
|
|
35832f8f7f | ||
|
|
7466d8cb3a | ||
|
|
ea3786457b | ||
|
|
01322146c0 | ||
|
|
26adc557bf | ||
|
|
36e52d8165 | ||
|
|
4c0211fa23 | ||
|
|
a3b4dcf762 | ||
|
|
5537c525ca | ||
|
|
3746fd88b5 | ||
|
|
e373144350 | ||
|
|
3be4bfc821 | ||
|
|
2c1595d0d5 | ||
|
|
f08933f93c | ||
|
|
51813b7d7e | ||
|
|
4e30418f79 | ||
|
|
0af370d736 | ||
|
|
ac04a032ad | ||
|
|
f6223a6f71 | ||
|
|
937cea5a01 | ||
|
|
846522037f | ||
|
|
b5a88d00af | ||
|
|
7391271107 | ||
|
|
991694aca5 | ||
|
|
29b13d19d6 | ||
|
|
b0ff74c799 | ||
|
|
75c60660e7 | ||
|
|
eacc840c54 | ||
|
|
6d09c0adcf | ||
|
|
fe1cf1105a | ||
|
|
9454334ddd | ||
|
|
cc482c589c | ||
|
|
b387bc1038 | ||
|
|
890b735985 | ||
|
|
eb81e00d20 | ||
|
|
c619bfc05c | ||
|
|
b5935eb4c5 | ||
|
|
73b3023c2d | ||
|
|
4cf709fc26 | ||
|
|
b471b934f2 | ||
|
|
abbd4c8934 | ||
|
|
ed8c852d35 | ||
|
|
c3598b3513 | ||
|
|
b5f6a9c91a | ||
|
|
d18810b612 | ||
|
|
3c17d713b3 | ||
|
|
d46b2d52ce | ||
|
|
e07d0b3b25 | ||
|
|
2ff3b24b33 | ||
|
|
115fb05c85 | ||
|
|
1e7e72883d | ||
|
|
1edb7639f6 | ||
|
|
33c5ceecec | ||
|
|
a8a7f306fe | ||
|
|
9888e51b93 | ||
|
|
c3124314ef | ||
|
|
975cd183bc | ||
|
|
aa9da0b0c8 | ||
|
|
d83b106607 | ||
|
|
1a0a6cedc7 | ||
|
|
d4e1a83a5a | ||
|
|
03e93d546a | ||
|
|
a1b957b0f7 | ||
|
|
eeffb11841 | ||
|
|
860dc7dbc2 | ||
|
|
220f267976 | ||
|
|
b5bd6e008b | ||
|
|
9d20bc6441 | ||
|
|
1c36734092 | ||
|
|
9102b6e6b8 | ||
|
|
cd78fa9762 | ||
|
|
407e2d4b7b | ||
|
|
62d6ee46c4 | ||
|
|
f15bf8166f | ||
|
|
7ce0b225ef | ||
|
|
f853b29fd9 | ||
|
|
f9cc8de93e | ||
|
|
a924a876ae | ||
|
|
1f15d2c66e | ||
|
|
ffd8354208 | ||
|
|
b3b1f4664c | ||
|
|
03410ded9a | ||
|
|
09c9671376 | ||
|
|
736b3b93e9 | ||
|
|
4a71ca6b81 | ||
|
|
a828f9b1f9 | ||
|
|
7ce5f8e8b6 | ||
|
|
08b7e036b4 | ||
|
|
b1722a085e | ||
|
|
9d985585b9 | ||
|
|
de057b7234 | ||
|
|
04ec49e18e | ||
|
|
34c7c25908 | ||
|
|
f74374e759 | ||
|
|
cce74f5631 | ||
|
|
003b9e48e1 | ||
|
|
28bf73cd5a | ||
|
|
511b982dce | ||
|
|
dcd176f95c | ||
|
|
d394aa8a15 | ||
|
|
c8b35d5ce1 | ||
|
|
6cd107c3e3 | ||
|
|
a884e32816 | ||
|
|
45d2d3cbb5 | ||
|
|
382f75d249 | ||
|
|
e90010f0c0 | ||
|
|
d6aaed1d82 | ||
|
|
5082ed3b9e | ||
|
|
a858e07db0 | ||
|
|
df8f785731 | ||
|
|
332674a4a1 | ||
|
|
a686849bb6 | ||
|
|
54d041701c | ||
|
|
496eec17a1 | ||
|
|
25eb67c582 | ||
|
|
a26fab3cce | ||
|
|
7cb67cf8fb | ||
|
|
96ec1e937f | ||
|
|
b6616ed2cc | ||
|
|
7e263af75f | ||
|
|
b4c51f3d41 | ||
|
|
1dfbaa1e02 | ||
|
|
bd717349a7 | ||
|
|
99d7752e25 | ||
|
|
22e58d7623 | ||
|
|
30ad142868 | ||
|
|
e084ff4f7b | ||
|
|
3bbec4081a | ||
|
|
45137d5506 | ||
|
|
e9d9a656ab | ||
|
|
ade458b820 | ||
|
|
b022dcbb70 | ||
|
|
b403a7a25d | ||
|
|
a6632632fa | ||
|
|
b863ea51ad | ||
|
|
796b66b057 | ||
|
|
2626dcbc5f | ||
|
|
105a758914 | ||
|
|
98cce186c7 | ||
|
|
474e13f8b9 | ||
|
|
09f87238dc | ||
|
|
2b3f87d6f2 | ||
|
|
9d3c823603 | ||
|
|
7e00d70f4d | ||
|
|
0828f747e3 | ||
|
|
d7de0c2578 | ||
|
|
4dfc82f684 | ||
|
|
92748de7d4 | ||
|
|
0e7e27f99d | ||
|
|
23147f2328 | ||
|
|
3811f41076 | ||
|
|
43a0a4f8e0 | ||
|
|
66d1af63b0 | ||
|
|
bc68f400f6 | ||
|
|
67ee3a5a67 | ||
|
|
b407893db5 | ||
|
|
1b4a41b522 | ||
|
|
ac7f8a6447 | ||
|
|
da57fcb641 | ||
|
|
f11c332cb4 | ||
|
|
3b4006b821 | ||
|
|
4c62d8c1b2 | ||
|
|
4f47d4482b | ||
|
|
ad062486ff | ||
|
|
33c8bdfabf | ||
|
|
0365c94407 | ||
|
|
c81c4f9114 | ||
|
|
d920953df1 | ||
|
|
f02a2e44d8 | ||
|
|
fb1aa9c028 | ||
|
|
d4a1d4cd7e | ||
|
|
8a6d9a1496 | ||
|
|
a8eefbc9f0 | ||
|
|
75521fe363 | ||
|
|
c75778943f | ||
|
|
0e6fa37ae4 | ||
|
|
819eea9456 | ||
|
|
25eb8dc9b0 | ||
|
|
40067f5aa2 | ||
|
|
1ffc10e44f | ||
|
|
5d5547ffef | ||
|
|
e83c28bf54 | ||
|
|
a3c0911529 | ||
|
|
ae23f0de03 | ||
|
|
edb5529df3 | ||
|
|
4d3fb77786 | ||
|
|
24ac7c1626 | ||
|
|
9b2ec62be9 | ||
|
|
4252b5e273 | ||
|
|
4e8cbe3db1 | ||
|
|
bdf8bbe26f | ||
|
|
990c05fc3d | ||
|
|
7bb4e22a77 | ||
|
|
736503df1b | ||
|
|
414455a8fb | ||
|
|
bc0ab88e74 | ||
|
|
01ec539065 | ||
|
|
b9792fc17d | ||
|
|
c656c3c087 | ||
|
|
a6c1d2d486 | ||
|
|
44bce59777 | ||
|
|
9b5e5aa474 | ||
|
|
a5d02998ad | ||
|
|
1d4c129e9c | ||
|
|
e9a6ca8ebc | ||
|
|
28340c80dd | ||
|
|
05f6fde467 | ||
|
|
56d353cb64 | ||
|
|
260d0cdc67 | ||
|
|
cdd92303b8 | ||
|
|
0dba37f4f7 | ||
|
|
7f3775a061 | ||
|
|
a7a10f4eaa | ||
|
|
5166083406 | ||
|
|
5c4d95ac0f | ||
|
|
71c4d74759 | ||
|
|
725e8221a5 | ||
|
|
92ae41cc13 | ||
|
|
333b9130fe | ||
|
|
60ae903cf3 | ||
|
|
24ea55b010 | ||
|
|
8231d07706 | ||
|
|
98df2b111e | ||
|
|
d8875f381b | ||
|
|
6e3d16173a | ||
|
|
a88b189664 | ||
|
|
48da00eb66 | ||
|
|
5322332c5d | ||
|
|
c92cfbb20a | ||
|
|
f673647072 | ||
|
|
7ea5a9bba3 | ||
|
|
aa002c5d60 | ||
|
|
64020758d9 | ||
|
|
a4f357fd80 | ||
|
|
4c6fdfd76a | ||
|
|
c972452310 | ||
|
|
6ba6a16836 | ||
|
|
c63f1dfc53 | ||
|
|
ac79c45529 | ||
|
|
7f58737f1f | ||
|
|
ead8a48436 | ||
|
|
178d33155f | ||
|
|
07ab8e508c | ||
|
|
ea1d4adfa6 | ||
|
|
f34a8ef0e5 | ||
|
|
0df4b39bcc | ||
|
|
07eca00bd5 | ||
|
|
cf8386aa50 | ||
|
|
44de6a5549 | ||
|
|
a41a9bcbf7 | ||
|
|
a679fae9c0 | ||
|
|
0f906f3937 | ||
|
|
c907daa741 | ||
|
|
df579de147 | ||
|
|
e884bdbbc4 | ||
|
|
84fc70e9a9 | ||
|
|
4fc5d3f03b | ||
|
|
9402516acd | ||
|
|
65f7541ec7 | ||
|
|
22da14e3f7 | ||
|
|
fddad09167 | ||
|
|
f36f065508 | ||
|
|
0c294eefae | ||
|
|
5a0333ddaf | ||
|
|
e0b9a9a82f | ||
|
|
ab07eb6f4a | ||
|
|
bddd800769 | ||
|
|
5533ebf86a | ||
|
|
2397fdc495 | ||
|
|
129548764e | ||
|
|
ed257e39d0 | ||
|
|
33eaf2e05c | ||
|
|
41d99d5108 | ||
|
|
edefa5f786 | ||
|
|
d7b47b49d2 | ||
|
|
74af7ef8b2 | ||
|
|
1cfc9b6139 | ||
|
|
db6bf547a9 | ||
|
|
2b20714f50 | ||
|
|
cfa72ad7d1 | ||
|
|
2e11a8b458 | ||
|
|
58fe95d6fd | ||
|
|
de65c8b2cf | ||
|
|
57510980ed | ||
|
|
26a7f9dd46 | ||
|
|
7fb2464d84 | ||
|
|
9e7137ce09 | ||
|
|
80747fc306 | ||
|
|
0d75f71d16 | ||
|
|
46c2288f50 | ||
|
|
d76c9ad1db | ||
|
|
3faa18c020 | ||
|
|
d699496a52 | ||
|
|
19bf5f1a23 | ||
|
|
080b56a0cb | ||
|
|
82d9b033f2 | ||
|
|
bf6e465970 | ||
|
|
3ef9824d8e | ||
|
|
4b0e88ce46 | ||
|
|
0c32a889a9 | ||
|
|
6186b41e3f | ||
|
|
6e44a91d0b | ||
|
|
810b980e6b | ||
|
|
22356940d9 | ||
|
|
cc30f7aa02 | ||
|
|
6e689400b6 | ||
|
|
50fbf8833b | ||
|
|
d7137990b9 | ||
|
|
8ecc107c06 | ||
|
|
1fa3837bb0 | ||
|
|
5aec5b084a | ||
|
|
b77add97db | ||
|
|
b306d808fb | ||
|
|
1729c3f76d | ||
|
|
2eb591391e | ||
|
|
075194fea8 | ||
|
|
28b376d2d2 | ||
|
|
4f3de4cf54 | ||
|
|
85f387bfb9 | ||
|
|
81aec8402b | ||
|
|
d407dfed0a | ||
|
|
dcd4d0886e | ||
|
|
9ad240951e | ||
|
|
e86d4657da | ||
|
|
d36e33a7cf | ||
|
|
376bcefc14 | ||
|
|
c0c110fe7e | ||
|
|
3dc6e576df | ||
|
|
49f717fcf8 | ||
|
|
00152e0db4 | ||
|
|
1964203848 | ||
|
|
495444abd0 | ||
|
|
09ec0b6325 | ||
|
|
cad0f15a83 | ||
|
|
b087dcd328 | ||
|
|
8dff263a0c | ||
|
|
d546b23bd8 | ||
|
|
bb9400745e | ||
|
|
30f84e78e1 | ||
|
|
30af020ba8 | ||
|
|
ef27cd6c5c | ||
|
|
770289cd67 | ||
|
|
25b5c96648 | ||
|
|
85cafda168 | ||
|
|
96e1b5b0f6 | ||
|
|
dd90e84f6a | ||
|
|
05969c2f99 | ||
|
|
05ef1c3924 | ||
|
|
8018920a64 | ||
|
|
dcb7e389eb | ||
|
|
a15cd62fd4 | ||
|
|
1c3a2e475d | ||
|
|
83c45d772f | ||
|
|
96c8d4de6a | ||
|
|
9558974080 | ||
|
|
285e298a8b | ||
|
|
57b17b1798 | ||
|
|
7e13642098 | ||
|
|
26b5f53b12 | ||
|
|
896b321cdc | ||
|
|
4115a5936c | ||
|
|
0e0cf7df09 | ||
|
|
0c232be0c4 | ||
|
|
67bc26ed57 | ||
|
|
f421a14659 | ||
|
|
81b74cf46c | ||
|
|
b1a3ce323b | ||
|
|
1f58826444 | ||
|
|
2ea853b1e6 | ||
|
|
a2e1647faa | ||
|
|
4e060203ad | ||
|
|
0540e2cf6c | ||
|
|
21e4679b6c | ||
|
|
8b9728b40b | ||
|
|
e0739709e7 | ||
|
|
41da6d552f | ||
|
|
ecb70b43df | ||
|
|
b2290f5b4c | ||
|
|
c6f415e9df | ||
|
|
3f7d622512 | ||
|
|
794d3ffcac | ||
|
|
ee2f87ef32 | ||
|
|
87da2a165d | ||
|
|
130deb8e22 | ||
|
|
b65d65cb12 | ||
|
|
3caa5776c8 | ||
|
|
6cbbb6498f | ||
|
|
fb12ee7f2a | ||
|
|
48842b6f2a | ||
|
|
fc90526258 | ||
|
|
efd1f24ab2 | ||
|
|
0808672b6e | ||
|
|
fe0de47805 | ||
|
|
ccf5b0ef53 | ||
|
|
218f4872b8 | ||
|
|
077569dd5d | ||
|
|
a3f79065c5 | ||
|
|
fbfcf862cb | ||
|
|
7307b84dad | ||
|
|
6b15a9b6d2 | ||
|
|
e3420fd563 | ||
|
|
31120f51c9 | ||
|
|
3d279ec127 | ||
|
|
48cd76b938 | ||
|
|
8e5fbc8480 | ||
|
|
cdea968c24 | ||
|
|
a6aabed6a1 | ||
|
|
cde235896f | ||
|
|
ca48aaf5ee | ||
|
|
8a705a6a9a | ||
|
|
b3adbc1110 | ||
|
|
2bbc13ccf6 | ||
|
|
411384f0e7 | ||
|
|
a9b0bc0409 | ||
|
|
228582296c | ||
|
|
d67108cd20 | ||
|
|
175f185cb1 | ||
|
|
56f27ec356 | ||
|
|
5253d67e04 | ||
|
|
a314f59db9 | ||
|
|
da4e268e63 | ||
|
|
c186378d43 | ||
|
|
482bb5f64f | ||
|
|
3c1492e9bc | ||
|
|
be301b825f | ||
|
|
fc875f7ad6 | ||
|
|
f644053811 | ||
|
|
c0e5e78d2b | ||
|
|
d103872c61 | ||
|
|
d47104f3f3 | ||
|
|
d4288f89d3 | ||
|
|
7060257051 | ||
|
|
866ec097c0 | ||
|
|
dcbe2805e6 | ||
|
|
262814391a | ||
|
|
b32cf403e6 | ||
|
|
921ee34779 | ||
|
|
75a871c437 | ||
|
|
b5a47efe8e | ||
|
|
8ff24d876c | ||
|
|
dd44bb2a0c | ||
|
|
f3686b23c7 | ||
|
|
fccdc8ca1f | ||
|
|
fc149666d9 | ||
|
|
4292838e80 | ||
|
|
befac94473 | ||
|
|
c0128d86f4 | ||
|
|
794636e208 | ||
|
|
eff6ac254f | ||
|
|
fa575be289 | ||
|
|
060ad68570 | ||
|
|
1620394736 | ||
|
|
1e6e91e4bd | ||
|
|
b0cbe2ae70 | ||
|
|
1391202985 | ||
|
|
d9ad24f563 | ||
|
|
61279752e2 | ||
|
|
1794b65182 | ||
|
|
6bb5629bb4 | ||
|
|
0f3db29e75 | ||
|
|
c6cdf77a68 | ||
|
|
48682faf71 | ||
|
|
49d396538c | ||
|
|
30802820b0 | ||
|
|
7fca0ba588 | ||
|
|
50d080d098 | ||
|
|
96792bec78 | ||
|
|
e6953c8883 | ||
|
|
1d9b1781d4 | ||
|
|
6720bb9f6b | ||
|
|
2217e5303d | ||
|
|
c8392a714c | ||
|
|
749a1d2da2 | ||
|
|
2920e33d71 | ||
|
|
1bc11a207a | ||
|
|
ddbd4e79a2 | ||
|
|
49c107bb5f | ||
|
|
1b99d28c9b | ||
|
|
7b8e983412 | ||
|
|
f1373234dd | ||
|
|
b511a52e09 | ||
|
|
4c8c2aa323 | ||
|
|
a28c9f8f36 | ||
|
|
9ff1dfe019 | ||
|
|
3ae52dacfc | ||
|
|
aa6f879504 | ||
|
|
1dde5c5689 | ||
|
|
913b422d74 | ||
|
|
2459bcd206 | ||
|
|
cce1ce0ee0 | ||
|
|
8518513aff | ||
|
|
ce5e28c543 | ||
|
|
9b7a8203f6 | ||
|
|
fd8f2422ea | ||
|
|
09325fadaf | ||
|
|
d7d7be9366 | ||
|
|
25afdda2b2 | ||
|
|
c690d5e940 | ||
|
|
243a8b60b1 | ||
|
|
7b317619ac | ||
|
|
c987bed965 | ||
|
|
ada4bafb94 | ||
|
|
27f5d0fd54 |
5
.github/workflows/bridge.yml
vendored
5
.github/workflows/bridge.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
env:
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
|
||||
jobs:
|
||||
generate_bridge:
|
||||
@@ -49,9 +50,9 @@ jobs:
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: ''
|
||||
components: "rustfmt"
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
|
||||
230
.github/workflows/build-macos-arm64.yml
vendored
Normal file
230
.github/workflows/build-macos-arm64.yml
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
name: Flutter Nightly MacOS Arm64 Build
|
||||
|
||||
on:
|
||||
#schedule:
|
||||
# schedule build every night
|
||||
# - cron: "0/6 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
CARGO_NDK_VERSION: "3.1.2"
|
||||
LLVM_VERSION: "15.0.6"
|
||||
FLUTTER_VERSION: "3.19.6"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
# for arm64 linux because official Dart SDK does not work
|
||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.03.25
|
||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
||||
VERSION: "1.2.4"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||
# To make a custom build with your own servers set the below secret values
|
||||
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
||||
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
||||
API_SERVER: "${{ secrets.API_SERVER }}"
|
||||
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||
|
||||
jobs:
|
||||
build-appimage:
|
||||
name: Build image ${{ matrix.job.target }}
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
arch: x86_64,
|
||||
}
|
||||
- {
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
arch: aarch64,
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Rename Binary
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y wget libarchive-tools
|
||||
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
|
||||
|
||||
- name: Patch archlinux PKGBUILD
|
||||
if: matrix.job.arch == 'x86_64'
|
||||
run: |
|
||||
sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD
|
||||
if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then
|
||||
sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD
|
||||
fi
|
||||
bsdtar -zxvf rustdesk-${{ env.VERSION }}.deb
|
||||
tar -xvf ./data.tar.xz
|
||||
case ${{ matrix.job.arch }} in
|
||||
aarch64)
|
||||
mkdir -p flutter/build/linux/arm64/release/bundle
|
||||
cp -rf usr/lib/rustdesk/* flutter/build/linux/arm64/release/bundle/
|
||||
;;
|
||||
x86_64)
|
||||
mkdir -p flutter/build/linux/x64/release/bundle
|
||||
cp -rf usr/lib/rustdesk/* flutter/build/linux/x64/release/bundle/
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Build archlinux package
|
||||
if: matrix.job.arch == 'x86_64'
|
||||
uses: rustdesk-org/arch-makepkg-action@master
|
||||
with:
|
||||
packages: >
|
||||
llvm
|
||||
clang
|
||||
libva
|
||||
libvdpau
|
||||
rust
|
||||
gstreamer
|
||||
unzip
|
||||
git
|
||||
cmake
|
||||
gcc
|
||||
curl
|
||||
wget
|
||||
nasm
|
||||
zip
|
||||
make
|
||||
pkg-config
|
||||
clang
|
||||
gtk3
|
||||
xdotool
|
||||
libxcb
|
||||
libxfixes
|
||||
alsa-lib
|
||||
pipewire
|
||||
python
|
||||
ttf-arphic-uming
|
||||
libappindicator-gtk3
|
||||
pam
|
||||
gst-plugins-base
|
||||
gst-plugin-pipewire
|
||||
scripts: |
|
||||
cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f
|
||||
|
||||
- name: Publish archlinux package
|
||||
if: matrix.job.arch == 'x86_64'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
res/rustdesk-${{ env.VERSION }}*.zst
|
||||
|
||||
- name: Build appimage package
|
||||
shell: bash
|
||||
run: |
|
||||
# set-up appimage-builder
|
||||
pushd /tmp
|
||||
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
||||
chmod +x appimage-builder-x86_64.AppImage
|
||||
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
|
||||
popd
|
||||
# run appimage-builder
|
||||
pushd appimage
|
||||
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
|
||||
|
||||
- name: Publish appimage package
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
|
||||
|
||||
build-flatpak:
|
||||
name: Build Flatpak ${{ matrix.job.target }}
|
||||
runs-on: ${{ matrix.job.on }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
distro: ubuntu18.04,
|
||||
on: ubuntu-20.04,
|
||||
arch: x86_64,
|
||||
}
|
||||
- {
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
|
||||
distro: ubuntu22.04,
|
||||
on: [self-hosted, Linux, ARM64],
|
||||
arch: aarch64,
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Rename Binary
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y wget
|
||||
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
|
||||
|
||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
||||
id: rpm
|
||||
with:
|
||||
arch: ${{ matrix.job.arch }}
|
||||
distro: ${{ matrix.job.distro }}
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "${PWD}"
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}:/workspace"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt-get update -y
|
||||
apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
rpm \
|
||||
wget
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
pushd /workspace
|
||||
# install
|
||||
apt-get update -y
|
||||
apt-get install -y \
|
||||
cmake \
|
||||
curl \
|
||||
flatpak \
|
||||
flatpak-builder \
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libgtk-3-dev \
|
||||
nasm \
|
||||
wget
|
||||
# flatpak deps
|
||||
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
|
||||
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
|
||||
# package
|
||||
pushd flatpak
|
||||
git clone https://github.com/flathub/shared-modules.git --depth=1
|
||||
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
||||
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
|
||||
|
||||
- name: Publish flatpak package
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -21,6 +21,9 @@ on:
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- "res/**"
|
||||
- "appimage/**"
|
||||
- "flatpak/**"
|
||||
|
||||
jobs:
|
||||
# ensure_cargo_fmt:
|
||||
@@ -71,12 +74,12 @@ jobs:
|
||||
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
@@ -103,6 +106,7 @@ jobs:
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libpam0g-dev \
|
||||
libasound2-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
|
||||
37
.github/workflows/clear-cache.yml
vendored
Normal file
37
.github/workflows/clear-cache.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Clear cache
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
clear-cache:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clear cache
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
console.log("About to clear")
|
||||
const caches = await github.rest.actions.getActionsCacheList({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
})
|
||||
for (const cache of caches.data.actions_caches) {
|
||||
console.log(cache)
|
||||
github.rest.actions.deleteActionsCacheById({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
cache_id: cache.id,
|
||||
})
|
||||
}
|
||||
console.log("Clear completed")
|
||||
|
||||
- name: Purge cache # Above seems not clear thouroughly, so add this to double clear
|
||||
uses: MyAlbum/purge-cache@v2
|
||||
with:
|
||||
accessed: true # Purge caches by their last accessed time (default)
|
||||
created: false # Purge caches by their created time (default)
|
||||
max-age: 1 # in seconds
|
||||
1751
.github/workflows/flutter-build.yml
vendored
1751
.github/workflows/flutter-build.yml
vendored
File diff suppressed because it is too large
Load Diff
3
.github/workflows/flutter-ci.yml
vendored
3
.github/workflows/flutter-ci.yml
vendored
@@ -13,6 +13,9 @@ on:
|
||||
- ".github/**"
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- "res/**"
|
||||
- "appimage/**"
|
||||
- "flatpak/**"
|
||||
|
||||
jobs:
|
||||
run-ci:
|
||||
|
||||
2
.github/workflows/flutter-tag.yml
vendored
2
.github/workflows/flutter-tag.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
upload-artifact: true
|
||||
upload-tag: ${{ env.GITHUB_REF_NAME }}
|
||||
upload-tag: ${{ github.ref_name }}
|
||||
|
||||
update-fdroid-version-file:
|
||||
name: Publish RustDesk version file for F-Droid updater
|
||||
|
||||
6
.github/workflows/history.yml
vendored
6
.github/workflows/history.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
|
||||
run: python3 .\build.py --portable --hwcodec --flutter
|
||||
|
||||
- name: Build self-extracted executable
|
||||
shell: bash
|
||||
|
||||
60
.github/workflows/third-party-RustDeskTempTopMostWindow.yml
vendored
Normal file
60
.github/workflows/third-party-RustDeskTempTopMostWindow.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: build RustDeskTempTopMostWindow
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
upload-artifact:
|
||||
type: boolean
|
||||
default: true
|
||||
target:
|
||||
description: 'Target'
|
||||
required: true
|
||||
type: string
|
||||
default: 'windows-2022'
|
||||
configuration:
|
||||
description: 'Configuration'
|
||||
required: true
|
||||
type: string
|
||||
default: 'Release'
|
||||
platform:
|
||||
description: 'Platform'
|
||||
required: true
|
||||
type: string
|
||||
default: 'x64'
|
||||
target_version:
|
||||
description: 'TargetVersion'
|
||||
required: true
|
||||
type: string
|
||||
default: 'Windows10'
|
||||
|
||||
env:
|
||||
project_path: WindowInjection/WindowInjection.vcxproj
|
||||
|
||||
jobs:
|
||||
build-RustDeskTempTopMostWindow:
|
||||
runs-on: ${{ inputs.target }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
env:
|
||||
build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }}
|
||||
steps:
|
||||
- name: Add MSBuild to PATH
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Download the source code
|
||||
run: |
|
||||
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
|
||||
|
||||
# Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3
|
||||
- name: Build the project
|
||||
run: |
|
||||
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796
|
||||
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
|
||||
|
||||
- name: Archive build artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
with:
|
||||
name: topmostwindow-artifacts
|
||||
path: |
|
||||
./${{ env.build_output_dir }}/WindowInjection.dll
|
||||
726
Cargo.lock
generated
726
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
46
Cargo.toml
46
Cargo.toml
@@ -4,7 +4,7 @@ version = "1.2.4"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
description = "A remote control software."
|
||||
description = "RustDesk Remote Desktop"
|
||||
default-run = "rustdesk"
|
||||
rust-version = "1.75"
|
||||
|
||||
@@ -20,18 +20,14 @@ path = "src/naming.rs"
|
||||
inline = []
|
||||
cli = []
|
||||
flutter_texture_render = []
|
||||
appimage = []
|
||||
flatpak = []
|
||||
use_samplerate = ["samplerate"]
|
||||
use_rubato = ["rubato"]
|
||||
use_dasp = ["dasp"]
|
||||
flutter = ["flutter_rust_bridge"]
|
||||
default = ["use_dasp"]
|
||||
hwcodec = ["scrap/hwcodec"]
|
||||
gpucodec = ["scrap/gpucodec"]
|
||||
vram = ["scrap/vram"]
|
||||
mediacodec = ["scrap/mediacodec"]
|
||||
linux_headless = ["pam" ]
|
||||
virtual_display_driver = ["virtual_display"]
|
||||
plugin_framework = []
|
||||
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
|
||||
unix-file-copy-paste = [
|
||||
@@ -45,7 +41,8 @@ unix-file-copy-paste = [
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
whoami = "1.4"
|
||||
async-trait = "0.1"
|
||||
whoami = "1.5.0"
|
||||
scrap = { path = "libs/scrap", features = ["wayland"] }
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
serde_derive = "1.0"
|
||||
@@ -57,7 +54,6 @@ lazy_static = "1.4"
|
||||
sha2 = "0.10"
|
||||
repng = "0.2"
|
||||
parity-tokio-ipc = { git = "https://github.com/rustdesk-org/parity-tokio-ipc" }
|
||||
runas = "=1.0" # https://github.com/mitsuhiko/rust-runas/issues/13
|
||||
magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" }
|
||||
dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
|
||||
rubato = { version = "0.12", optional = true }
|
||||
@@ -65,7 +61,6 @@ samplerate = { version = "0.2", optional = true }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
clap = "4.2"
|
||||
rpassword = "7.2"
|
||||
base64 = "0.21"
|
||||
num_cpus = "1.15"
|
||||
bytes = { version = "1.4", features = ["serde"] }
|
||||
default-net = "0.14"
|
||||
@@ -95,18 +90,32 @@ sys-locale = "0.3"
|
||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||
clipboard = { path = "libs/clipboard" }
|
||||
ctrlc = "3.2"
|
||||
arboard = { version = "3.2", features = ["wayland-data-control"] }
|
||||
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
|
||||
system_shutdown = "4.0"
|
||||
qrcode-generator = "4.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] }
|
||||
winapi = { version = "0.3", features = [
|
||||
"winuser",
|
||||
"wincrypt",
|
||||
"shellscalingapi",
|
||||
"pdh",
|
||||
"synchapi",
|
||||
"memoryapi",
|
||||
"shellapi",
|
||||
"devguid",
|
||||
"setupapi",
|
||||
"cguid",
|
||||
"cfgmgr32",
|
||||
"ioapiset",
|
||||
] }
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
virtual_display = { path = "libs/virtual_display", optional = true }
|
||||
virtual_display = { path = "libs/virtual_display" }
|
||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
||||
shared_memory = "0.12"
|
||||
tauri-winrt-notification = "0.1.2"
|
||||
runas = "1.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2"
|
||||
@@ -131,10 +140,10 @@ wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false }
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
@@ -145,7 +154,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
|
||||
evdev = { git="https://github.com/fufesou/evdev" }
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
pam = { git="https://github.com/fufesou/pam", optional = true }
|
||||
pam = { git="https://github.com/fufesou/pam" }
|
||||
users = { version = "0.11" }
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
@@ -162,9 +171,10 @@ members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "lib
|
||||
exclude = ["vdi/host", "examples/custom_plugin"]
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2023 Purslane, Inc."
|
||||
# this FileDescription overrides package.description
|
||||
FileDescription = "RustDesk"
|
||||
LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
|
||||
ProductName = "RustDesk"
|
||||
FileDescription = "RustDesk Remote Desktop"
|
||||
OriginalFilename = "rustdesk.exe"
|
||||
|
||||
[target.'cfg(target_os="windows")'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
28
README.md
28
README.md
@@ -13,8 +13,6 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
@@ -31,22 +29,6 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Free Public Servers
|
||||
|
||||
Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow.
|
||||
| Location | Vendor | Specification |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
||||
|
||||
Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Desktop versions use Flutter or Sciter (deprecated) for GUI, this tutorial is for Sciter only, since it is easier and more friendly to start. Check out our [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for building Flutter version.
|
||||
@@ -180,12 +162,12 @@ Please ensure that you are running these commands from the root of the RustDesk
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
|
||||
|
||||
## Snapshots
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
version: 1
|
||||
script:
|
||||
- rm -rf ./AppDir || true
|
||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
||||
- bsdtar -zxvf rustdesk.deb
|
||||
- tar -xvf ./data.tar.xz
|
||||
- mkdir ./AppDir
|
||||
- mv ./usr ./AppDir/usr
|
||||
@@ -26,18 +26,18 @@ AppDir:
|
||||
- arm64
|
||||
allow_unauthenticated: true
|
||||
sources:
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted
|
||||
universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted
|
||||
universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
include:
|
||||
- libc6
|
||||
- libc6:arm64
|
||||
- libgtk-3-0
|
||||
- libxcb-randr0
|
||||
- libxdo3
|
||||
@@ -51,9 +51,15 @@ AppDir:
|
||||
- libva-x11-2
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- gstreamer1.0-pipewire
|
||||
- libwayland-client0
|
||||
- libwayland-cursor0
|
||||
- libwayland-egl1
|
||||
- libpulse0
|
||||
- packagekit-gtk3-module
|
||||
- libcanberra-gtk3-module
|
||||
- libpam0g
|
||||
- libdrm2
|
||||
exclude:
|
||||
- humanity-icon-theme
|
||||
- hicolor-icon-theme
|
||||
@@ -69,8 +75,11 @@ AppDir:
|
||||
- usr/share/doc/*/TODO.*
|
||||
runtime:
|
||||
env:
|
||||
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
|
||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
|
||||
GDK_BACKEND: x11
|
||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
|
||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||
test:
|
||||
fedora-30:
|
||||
image: appimagecrafters/tests-env:fedora-30
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
version: 1
|
||||
script:
|
||||
- rm -rf ./AppDir || true
|
||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
||||
- bsdtar -zxvf rustdesk.deb
|
||||
- tar -xvf ./data.tar.xz
|
||||
- mkdir ./AppDir
|
||||
- mv ./usr ./AppDir/usr
|
||||
@@ -26,18 +26,16 @@ AppDir:
|
||||
- amd64
|
||||
allow_unauthenticated: true
|
||||
sources:
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates universe
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted
|
||||
universe multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
||||
universe multiverse
|
||||
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
|
||||
bionic main
|
||||
include:
|
||||
- libc6:amd64
|
||||
- libgtk-3-0
|
||||
@@ -54,9 +52,14 @@ AppDir:
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- gstreamer1.0-pipewire
|
||||
- libwayland-client0
|
||||
- libwayland-cursor0
|
||||
- libwayland-egl1
|
||||
- libpulse0
|
||||
- packagekit-gtk3-module
|
||||
- libcanberra-gtk3-module
|
||||
- libpam0g
|
||||
- libdrm2
|
||||
exclude:
|
||||
- humanity-icon-theme
|
||||
- hicolor-icon-theme
|
||||
@@ -72,8 +75,11 @@ AppDir:
|
||||
- usr/share/doc/*/TODO.*
|
||||
runtime:
|
||||
env:
|
||||
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
|
||||
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
|
||||
GDK_BACKEND: x11
|
||||
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
|
||||
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||
test:
|
||||
fedora-30:
|
||||
image: appimagecrafters/tests-env:fedora-30
|
||||
|
||||
54
build.py
54
build.py
@@ -33,9 +33,9 @@ def get_arch() -> str:
|
||||
|
||||
|
||||
def system2(cmd):
|
||||
err = os.system(cmd)
|
||||
if err != 0:
|
||||
print(f"Error occurred when executing: {cmd}. Exiting.")
|
||||
exit_code = os.system(cmd)
|
||||
if exit_code != 0:
|
||||
sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n")
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
@@ -80,8 +80,10 @@ def parse_rc_features(feature):
|
||||
return get_all_features()
|
||||
elif isinstance(feature, list):
|
||||
if windows:
|
||||
# download third party is deprecated, we use github ci instead.
|
||||
# force add PrivacyMode
|
||||
feature.append('PrivacyMode')
|
||||
# feature.append('PrivacyMode')
|
||||
pass
|
||||
for feat in feature:
|
||||
if isinstance(feat, str) and feat.upper() == 'ALL':
|
||||
return get_all_features()
|
||||
@@ -109,6 +111,8 @@ def make_parser():
|
||||
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
||||
parser.add_argument('--flutter', action='store_true',
|
||||
help='Build flutter package', default=False)
|
||||
parser.add_argument('--disable-flutter-texture-render', action='store_true',
|
||||
help='Build flutter package', default=False)
|
||||
parser.add_argument(
|
||||
'--hwcodec',
|
||||
action='store_true',
|
||||
@@ -116,9 +120,9 @@ def make_parser():
|
||||
'' if windows or osx else ', need libva-dev, libvdpau-dev.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--gpucodec',
|
||||
'--vram',
|
||||
action='store_true',
|
||||
help='Enable feature gpucodec, only available on windows now.'
|
||||
help='Enable feature vram, only available on windows now.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--portable',
|
||||
@@ -130,21 +134,17 @@ def make_parser():
|
||||
action='store_true',
|
||||
help='Build with unix file copy paste feature'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--flatpak',
|
||||
action='store_true',
|
||||
help='Build rustdesk libs with the flatpak feature enabled'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--appimage',
|
||||
action='store_true',
|
||||
help='Build rustdesk libs with the appimage feature enabled'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-cargo',
|
||||
action='store_true',
|
||||
help='Skip cargo build process, only flutter version + Linux supported currently'
|
||||
)
|
||||
if windows:
|
||||
parser.add_argument(
|
||||
'--skip-portable-pack',
|
||||
action='store_true',
|
||||
help='Skip packing, only flutter version + Windows supported'
|
||||
)
|
||||
parser.add_argument(
|
||||
"--package",
|
||||
type=str
|
||||
@@ -188,6 +188,9 @@ def generate_build_script_for_docker():
|
||||
system2("bash /tmp/build.sh")
|
||||
|
||||
|
||||
# Downloading third party resources is deprecated.
|
||||
# We can use this function in an offline build environment.
|
||||
# Even in an online environment, we recommend building third-party resources yourself.
|
||||
def download_extract_features(features, res_dir):
|
||||
import re
|
||||
|
||||
@@ -271,15 +274,12 @@ def get_features(args):
|
||||
features = ['inline'] if not args.flutter else []
|
||||
if args.hwcodec:
|
||||
features.append('hwcodec')
|
||||
if args.gpucodec:
|
||||
features.append('gpucodec')
|
||||
if args.vram:
|
||||
features.append('vram')
|
||||
if args.flutter:
|
||||
features.append('flutter')
|
||||
features.append('flutter_texture_render')
|
||||
if args.flatpak:
|
||||
features.append('flatpak')
|
||||
if args.appimage:
|
||||
features.append('appimage')
|
||||
if not args.disable_flutter_texture_render:
|
||||
features.append('flutter_texture_render')
|
||||
if args.unix_file_copy_paste:
|
||||
features.append('unix-file-copy-paste')
|
||||
print("features:", features)
|
||||
@@ -410,9 +410,11 @@ def build_flutter_dmg(version, features):
|
||||
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
||||
os.chdir('flutter')
|
||||
system2('flutter build macos --release')
|
||||
'''
|
||||
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")
|
||||
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
|
||||
'''
|
||||
os.chdir("..")
|
||||
|
||||
|
||||
@@ -427,7 +429,7 @@ def build_flutter_arch_manjaro(version, features):
|
||||
system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
|
||||
|
||||
|
||||
def build_flutter_windows(version, features):
|
||||
def build_flutter_windows(version, features, skip_portable_pack):
|
||||
if not skip_cargo:
|
||||
system2(f'cargo build --features {features} --lib --release')
|
||||
if not os.path.exists("target/release/librustdesk.dll"):
|
||||
@@ -438,6 +440,8 @@ def build_flutter_windows(version, features):
|
||||
os.chdir('..')
|
||||
shutil.copy2('target/release/deps/dylib_virtual_display.dll',
|
||||
flutter_build_dir_2)
|
||||
if skip_portable_pack:
|
||||
return
|
||||
os.chdir('libs/portable')
|
||||
system2('pip3 install -r requirements.txt')
|
||||
system2(
|
||||
@@ -487,7 +491,7 @@ def main():
|
||||
os.chdir('../../..')
|
||||
|
||||
if flutter:
|
||||
build_flutter_windows(version, features)
|
||||
build_flutter_windows(version, features, args.skip_portable_pack)
|
||||
return
|
||||
system2('cargo build --release --features ' + features)
|
||||
# system2('upx.exe target/release/rustdesk.exe')
|
||||
|
||||
4
build.rs
4
build.rs
@@ -1,9 +1,11 @@
|
||||
#[cfg(windows)]
|
||||
fn build_windows() {
|
||||
let file = "src/platform/windows.cc";
|
||||
cc::Build::new().file(file).compile("windows");
|
||||
let file2 = "src/platform/windows_delete_test_cert.cc";
|
||||
cc::Build::new().file(file).file(file2).compile("windows");
|
||||
println!("cargo:rustc-link-lib=WtsApi32");
|
||||
println!("cargo:rerun-if-changed={}", file);
|
||||
println!("cargo:rerun-if-changed={}", file2);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -27,14 +27,6 @@
|
||||
|
||||
[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## خوادم مفتوحة ومجانية
|
||||
|
||||
فيما يلي الخوادم التي تستخدمها مجانًا، وقد تتغير طوال الوقت. إذا لم تكن قريبًا من أحد هؤلاء، فقد تكون شبكتك بطيئة.
|
||||
| الموقع | المورد | المواصفات |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
|
||||
|
||||
## التبعيات
|
||||
|
||||
لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم
|
||||
|
||||
@@ -22,14 +22,6 @@ Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se d
|
||||
|
||||
[**STAHOVÁNÍ ZKOMPILOVANÝCH APLIKACÍ**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Veřejné, zdarma službu nabízející servery
|
||||
|
||||
Níže jsou uvedeny servery zdarma k vašemu použití (údaje se mohou v čase měnit). Pokud se nenacházíte v oblastech světa poblíž nich, spojení může být pomalé.
|
||||
| umístění | dodavatel | parametry |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
|
||||
|
||||
## Softwarové součásti, na kterých závisí
|
||||
|
||||
Varianta pro počítač používá pro grafické uživatelské rozhraní [sciter](https://sciter.com/) – stáhněte si potřebnou knihovnu.
|
||||
|
||||
@@ -19,14 +19,6 @@ RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONT
|
||||
|
||||
[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Gratis offentlige servere
|
||||
|
||||
Nedenfor er de servere, du bruger gratis, det kan ændre sig med tiden. Hvis du ikke er tæt på en af disse, kan dit netværk være langsomt.
|
||||
|
||||
| Beliggenhed | Udbyder | Specifikation |
|
||||
| ---------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Afhængigheder
|
||||
|
||||
Desktopversioner bruger [sciter](https://sciter.com/) eller Flutter til GUI, denne vejledning er kun for Sciter.
|
||||
|
||||
@@ -29,22 +29,6 @@ RustDesk heißt jegliche Mitarbeit willkommen. Schauen Sie sich [CONTRIBUTING-DE
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Freie öffentliche Server
|
||||
|
||||
Nachfolgend sind die Server gelistet, die Sie kostenlos nutzen können. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls Sie nicht in der Nähe einer dieser Server sind, kann es sein, dass Ihre Verbindung langsam sein wird.
|
||||
| Standort | Anbieter | Spezifikation |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Deutschland | [Hetzner](https://www.hetzner.com/de/) | 2 vCPU / 4 GB RAM |
|
||||
| Ukraine (Kiew) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
|
||||
|
||||
## Dev-Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Wenn Sie VS Code und Docker bereits installiert haben, können Sie auf das Abzeichen oben klicken, um loszulegen. Wenn Sie darauf klicken, wird VS Code automatisch die Dev-Container-Erweiterung installieren, den Quellcode in ein Container-Volume klonen und einen Dev-Container für die Verwendung aufsetzen.
|
||||
|
||||
Weitere Informationen finden Sie in [DEVCONTAINER-DE.md](DEVCONTAINER-DE.md).
|
||||
|
||||
## Abhängigkeiten
|
||||
|
||||
Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter.
|
||||
|
||||
@@ -19,14 +19,6 @@ RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`docs/CONTRIBUTING.md`](CONTRIBUT
|
||||
|
||||
[**BINARA ELŜUTO**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Senpagaj publikaj serviloj
|
||||
|
||||
Malsupre estas la serviloj, kiuj vi uzas senpage, ĝi povas ŝanĝi laŭlonge de la tempo. Se vi ne estas proksima de unu de tiuj, via reto povas esti malrapida.
|
||||
| Situo | Vendanto | Detaloj |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependantaĵoj
|
||||
|
||||
La labortabla versio uzas [sciter](https://sciter.com/) por la interfaco, bonvolu elŝuti la bibliotekon dinamikan sciter.
|
||||
|
||||
@@ -25,15 +25,6 @@ RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Servidores gratis de uso público
|
||||
|
||||
A continuación se muestran los servidores gratuitos, pueden cambiar a medida que pasa el tiempo. Si no estás cerca de uno de ellos, tu conexión puede ser lenta.
|
||||
|
||||
| Ubicación | Compañía | Especificación |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependencias
|
||||
|
||||
La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter.
|
||||
|
||||
@@ -25,13 +25,6 @@
|
||||
|
||||
[دریافت نرمافزار](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## سرورهای عمومی رایگان
|
||||
|
||||
شما ميتوانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر میکند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد.
|
||||
| موقعیت | سرویس دهنده | مشخصات |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| آلمان | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## وابستگی ها
|
||||
|
||||
نسخههای رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده میکنند. خواهشمندیم کتابخانهی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید.
|
||||
|
||||
@@ -19,14 +19,6 @@ RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`docs
|
||||
|
||||
[**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Vapaita julkisia palvelimia
|
||||
|
||||
Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan mittaan. Jos et ole lähellä yhtä näistä, verkkosi voi olla hidas.
|
||||
| Sijainti | Myyjä | Määrittely |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Riippuvuudet
|
||||
|
||||
Desktop-versiot käyttävät [sciter](https://sciter.com/) graafisena käyttöliittymänä, lataa sciter-dynaaminen kirjasto itsellesi.
|
||||
|
||||
@@ -19,14 +19,6 @@ RustDesk accueille les contributions de tout le monde. Voir [`docs/CONTRIBUTING.
|
||||
|
||||
[**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Serveurs publics libres
|
||||
|
||||
Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut changer au fil du temps. Si vous n'êtes pas proche de l'un d'entre eux, votre réseau peut être lent.
|
||||
|
||||
| Location | Vendor | Specification |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dépendances
|
||||
|
||||
Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface graphique, veuillez télécharger la bibliothèque dynamique sciter vous-même.
|
||||
|
||||
@@ -29,22 +29,6 @@
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Δωρεάν δημόσιοι διακομιστές
|
||||
|
||||
Παρακάτω είναι οι διακομιστές που χρησιμοποιούνται δωρεάν, ενδέχεται να αλλάξουν με την πάροδο του χρόνου. Εάν δεν είστε κοντά σε ένα από αυτούς, το δίκτυό σας ίσως να είναι αργό.
|
||||
| Περιοχή | Πάροχος | Προδιαγραφές |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Γερμανία | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ουκρανία (Κίεβο) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Αν έχετε εγκατεστημένα το VS Code και το Docker, μπορείτε να ξεκινήσετε κάνοντας κλικ στην παραπάνω εικόνα. Αυτό θα έχει ως αποτέλεσμα, το VS Code να εγκαταστήσει αυτόματα την επέκταση Dev Containers, εάν χρειάζεται, θα κλωνοποιήσει τον πηγαίο κώδικα σε έναν νέο container και θα εκκινήσει ένα Dev Container για χρήση προγραμματισμού.
|
||||
|
||||
Για περισσότερες πληροφορίες μεταβείτε στο [DEVCONTAINER.md](docs/DEVCONTAINER.md).
|
||||
|
||||
## Προαπαιτούμενα για build
|
||||
|
||||
Στις παραθυρικές εκδόσεις χρησιμοποιείται είτε το [sciter](https://sciter.com/) είτε το Flutter, τα παρακάτω βήματα είναι μόνο για το Sciter.
|
||||
|
||||
@@ -27,14 +27,6 @@ A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lás
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Ingyenes publikus szerverek
|
||||
|
||||
Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingyen használhatsz. Ezek a szerverek változhatnak a jövőben, illetve a hálózatuk lehet hogy lassú lehet.
|
||||
| Hely | Host | Specifikáció |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependencies
|
||||
|
||||
Az asztali verziók [sciter](https://sciter.com/)-t használnak a GUI-hoz, kérlek telepítsd a dynamikus könyvtárat magad.
|
||||
|
||||
@@ -31,20 +31,6 @@ RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTIN
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Server Publik Gratis
|
||||
|
||||
Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring dengan waktu mungkin akan terjadi perubahan spesifikasi pada setiap server yang ada. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote.
|
||||
| Lokasi | Penyedia | Spesifikasi |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
|
||||
| Ukraina (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Apabila PC kamu sudah terinstal VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya.
|
||||
|
||||
## Dependensi
|
||||
|
||||
Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<a href="#passaggi-per-la-compilazione">Compilazione</a> •
|
||||
<a href="#come-compilare-con-docker">Docker</a> •
|
||||
<a href="#struttura-dei-file">Struttura</a> •
|
||||
<a href="#screenshots">Schermate</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-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="#schermate">Schermate</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-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>Abbiamo bisogno del tuo aiuto per tradurre questo file README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI RustDesk</a> nella tua lingua nativa</b>
|
||||
</p>
|
||||
|
||||
@@ -13,28 +13,29 @@ Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Ancora un altro software per il controllo remoto del desktop, scritto in Rust.
|
||||
Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza.
|
||||
Puoi usare il nostro server rendezvous/relay, [configurare il tuo server](https://rustdesk.com/server) o [realizzare il tuo server rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
||||
|
||||
RustDesk accoglie il contributo di tutti.
|
||||
Per ulteriori informazioni su come iniziare a contribuire, vedi [`docs/CONTRIBUTING-IT.md`](CONTRIBUTING.md).
|
||||
Ancora un altro software per il controllo remoto del desktop, scritto in Rust. Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi usare il nostro server rendezvous/relay, [configurare il tuo server](https://rustdesk.com/server) o [realizzare il tuo server rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
[**DOWNLOAD PROGRAMMA**](https://github.com/rustdesk/rustdesk/releases)
|
||||

|
||||
|
||||
## Server pubblici gratuiti
|
||||
RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come iniziare a contribuire, vedi [CONTRIBUTING.md](CONTRIBUTING-IT.md).
|
||||
|
||||
Qui sotto trovi i server che possono essere usati gratuitamente, la lista potrebbe cambiare nel tempo.
|
||||
Se non sei vicino a uno di questi server, la connessione potrebbe essere lenta.
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
| Posizione | Venditore | Specifiche |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germania | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ucraina (Kyev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
[**SCARICA PROGRAMMA**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**SCARICA NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Dipendenze
|
||||
|
||||
La versione Desktop usa per la GUI [sciter](https://sciter.com/), per favore scarica la libreria dinamica sciter.
|
||||
Le versioni desktop utilizzano Flutter o Sciter (deprecato) per l'interfaccia utente, questo tutorial è solo per Sciter, poiché è più facile per iniziare. Controlla il nostro [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) per la compilazione della versione Flutter.
|
||||
|
||||
Scarica la libreria dinamica Sciter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
@@ -51,12 +52,22 @@ La versione Desktop usa per la GUI [sciter](https://sciter.com/), per favore sca
|
||||
|
||||
- Esegui `cargo run`
|
||||
|
||||
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Come compilare in Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
@@ -125,10 +136,7 @@ Quindi, ogni volta che devi compilare l'applicazione, esegui il seguente comando
|
||||
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
|
||||
```
|
||||
|
||||
Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci.
|
||||
Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione `<OPTIONAL-ARGS>`.
|
||||
Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`.
|
||||
L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con:
|
||||
Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci. Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione `<OPTIONAL-ARGS>`. Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`. L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
@@ -140,19 +148,21 @@ Oppure, se stai avviando un eseguibile di rilascio:
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste.
|
||||
Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
|
||||
Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste. Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
|
||||
|
||||
## Struttura dei file
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funzioni per il trasferimento file, e altre funzioni utili.
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: cattura dello schermo
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controllo tastiera/mouse specifico della piattaforma
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementazione del copia e incolla dei file per Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Sciter UI obsoleto (deprecato)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servizi audio/appunti/input/video e connessioni di rete
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avvio di una connessione peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter
|
||||
|
||||
## Schermate
|
||||
|
||||
|
||||
@@ -24,13 +24,6 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## 無料のパブリックサーバー
|
||||
|
||||
下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。
|
||||
| Location | Vendor | Specification |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## 依存関係
|
||||
|
||||
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。
|
||||
|
||||
@@ -24,13 +24,6 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## 무료 퍼블릭 서버
|
||||
|
||||
표에 있는 서버는 무료로 사용할 수 있지만 추후 변경될 수도 있습니다. 이 서버에서 멀다면, 네트워크가 느려질 가능성도 있습니다.
|
||||
| Location | Vendor | Specification |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## 의존관계
|
||||
|
||||
데스크탑판에는 GUI에 [sciter](https://sciter.com/)가 사용되었습니다. sciter dynamic library 를 다운로드해주세요.
|
||||
|
||||
@@ -19,13 +19,6 @@
|
||||
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## സൗജന്യ പൊതു സെർവറുകൾ
|
||||
|
||||
നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്വർക്ക് സ്ലോ ആയേക്കാം.
|
||||
| സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## ഡിപെൻഡൻസികൾ
|
||||
|
||||
ഡെസ്ക്ടോപ്പ് പതിപ്പുകൾ GUI-യ്ക്കായി [sciter](https://sciter.com/) ഉപയോഗിക്കുന്നു, ദയവായി സ്സൈറ്റർ ഡൈനാമിക് ലൈബ്രറി സ്വയം ഡൗൺലോഡ് ചെയ്യുക.
|
||||
|
||||
@@ -27,22 +27,6 @@ RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](CONTRIB
|
||||
alt="Download het op F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Gratis openbare servers
|
||||
|
||||
Hieronder staan de servers die u gratis gebruikt, ze kunnen in de loop van de tijd veranderen. Als u niet in de buurt van een van deze servers bevindt, kan uw vervinding langzamer zijn.
|
||||
| Locatie | Aanbieder | Specificaties |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Duitsland | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Oekraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Als u VS Code en Docker al hebt geinstalleerd, kunt u op de bovenstaande badge klikken om te beginnen. Door te klikken zal VS Code automatisch de Dev Containers-extensie installeren indien nodig, de broncode klonen naar een containervolume en een dev container opstarten voor gebruik.
|
||||
|
||||
Bekijk [DEVCONTAINER.md](docs/DEVCONTAINER.md) voor meer informatie.
|
||||
|
||||
## Afhankelijkheden
|
||||
|
||||
Desktop versies gebruiken [sciter](https://sciter.com/) of Flutter voor GUI, deze handleiding is alleen voor Sciter.
|
||||
|
||||
@@ -29,22 +29,6 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](C
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Darmowe Serwery Publiczne
|
||||
|
||||
Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska.
|
||||
| Lokalizacja | Dostawca | Specyfikacja |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Niemcy | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Ukraina (Kijów) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Konterner Programisty (Dev Container)
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Jeżeli masz zainstalowany VS Code i Docker, możesz kliknąć w powyższy link, aby rozpocząć. Kliknięcie spowoduje automatyczną instalację rozszrzenia Kontenera Programisty w VS Code (jeżeli wymagany), sklonuje kod źródłowy do kontenera, i przygotuje kontener do użycia.
|
||||
|
||||
Więcej informacji w pliku [DEVCONTAINER-PL.md](docs/DEVCONTAINER-PL.md) for more info.
|
||||
|
||||
## Zależności
|
||||
|
||||
Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać samodzielnie bibliotekę sciter.
|
||||
|
||||
@@ -19,14 +19,6 @@ RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](CONTRIBU
|
||||
|
||||
[**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
## Servidores Públicos Grátis
|
||||
|
||||
Abaixo estão os servidores que você está utilizando de graça, ele pode mudar com o tempo. Se você não está próximo de algum deles, sua conexão pode ser lenta.
|
||||
|
||||
| Localização | Fornecedor | Especificações |
|
||||
| ----------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependências
|
||||
|
||||
Versões de desktop utilizam [sciter](https://sciter.com/) para a GUI, por favor baixe a biblioteca dinâmica sciter por conta própria.
|
||||
|
||||
@@ -28,13 +28,6 @@ RustDesk приветствует вклад каждого. Ознакомьт
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Бесплатные общедоступные серверы
|
||||
|
||||
Ниже приведены бесплатные публичные сервера, используемые по умолчанию. Имейте ввиду, они могут меняться со временем. Также стоит отметить, что скорость работы сети зависит от вашего местоположения и расстояния до серверов. Подключение происходит к ближайшему доступному.
|
||||
| Расположение | Поставщик | Технические характеристики |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Германия | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Зависимости
|
||||
|
||||
Настольные версии используют [sciter](https://sciter.com/) для графического интерфейса, загрузите динамическую библиотеку sciter самостоятельно.
|
||||
|
||||
@@ -30,23 +30,6 @@ RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs
|
||||
alt="F-Droid'de Alın"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Ücretsiz Genel Sunucular
|
||||
|
||||
Aşağıda ücretsiz olarak kullandığınız sunucular listelenmiştir, zaman içinde değişebilirler. Eğer bunlardan birine yakın değilseniz, ağınız yavaş olabilir.
|
||||
| Konum | Sağlayıcı | Özellikler |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Almanya | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
|
||||
| Almanya | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
|
||||
| Ukrayna (Kiev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
|
||||
|
||||
## Geliştirici Konteyneri
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Eğer zaten VS Code ve Docker kurulu ise yukarıdaki rozete tıklayarak başlayabilirsiniz. Tıklamak, VS Code'un gerektiğinde Dev Konteyner eklentisini otomatik olarak yüklemesine, kaynak kodunu bir konteyner hacmine klonlamasına ve kullanım için bir geliştirici konteyneri başlatmasına neden olur.
|
||||
|
||||
Daha fazla bilgi için [DEVCONTAINER.md](docs/DEVCONTAINER-TR.md) belgesine bakabilirsiniz.
|
||||
|
||||
## Bağımlılıklar
|
||||
|
||||
Masaüstü sürümleri GUI için
|
||||
|
||||
@@ -31,22 +31,6 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Безкоштовні загальнодоступні сервери
|
||||
|
||||
Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно.
|
||||
| Місцезнаходження | Постачальник | Технічні характеристики |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Німеччина | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
|
||||
| Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Якщо у вас уже встановлено VS Code та Docker, ви можете натиснути значок вище, щоб розпочати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує вихідний код у том контейнера та розгорне контейнер dev для використання.
|
||||
|
||||
Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інформації
|
||||
|
||||
## Залежності
|
||||
|
||||
Стільничні версії використовують Flutter чи Sciter (застаріле) для графічного інтерфейсу. Ця інструкція лише для Sciter, оскільки він є більш простим та дружнім для початківців. Перегляньте [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) для збірки версії на Flutter.
|
||||
|
||||
@@ -1,64 +1,55 @@
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn"><br>
|
||||
<a href="#free-public-servers">Máy chủ</a> •
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Server</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Cấu trúc tệp tin</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và <a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b>
|
||||
<b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
|
||||
</p>
|
||||
|
||||
Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
**RustDesk** luôn hoan nghênh mọi đóng góp từ mọi người. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu.
|
||||
|
||||
[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
||||
|
||||
[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Các Máy Chủ Công Khai Miễn Phí
|
||||
|
||||
Dưới đây là những máy chủ mà bạn có thể sử dụng mà không mất phí, chú ý là máy chủ có thể thay đổi theo thời gian. Nếu địa điểm của bạn không gần một trong số những máy chủ này, thì kết nói có thể chậm.
|
||||
|
||||
| Địa điểm | Nhà cung cấp | Cấu hình |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Dependencies
|
||||
|
||||
Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bạn cần tự tải về thư viện sciter.
|
||||
Phiên bản máy tính sử dụng __Flutter__ hoặc __Sciter__ (đã lỗi thời) cho giao diện người dùng (GUI). Hướng dẫn này chỉ áp dụng cho phiên bản Sciter, vì nó thân thiện và dễ bắt đầu hơn. Hãy kiểm tra [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) của chúng tôi để xây dựng phiên bản Flutter.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
Vui lòng tự tải thư viện `Sciter` về máy theo hướng dẫn cho từng hệ điều hành.
|
||||
|
||||
Phiên bản cho điện thoại sử dụng Flutter. Chúng tôi sẽ chuyển sang sử dụng Flutter thay cho Sciter cho phiên bản máy tính.
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Cách để build
|
||||
## Các bước build cơ bản
|
||||
|
||||
- Chuẩn bị môi trường phát triển Rust và môi trường build C++
|
||||
- Chuẩn bị môi trường phát triển Rust và môi trường biên dịch C++
|
||||
|
||||
- Tải và cài [vcpkg](https://github.com/microsoft/vcpkg), và đặt biến môi trường `VCPKG_ROOT` sao cho đúng.
|
||||
|
||||
- Đối với Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||
- Đối với Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
||||
- Tải và cài đặt [`vcpkg`](https://github.com/microsoft/vcpkg), và thiết lập biến môi trường `VCPKG_ROOT`.
|
||||
|
||||
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
|
||||
- Linux/MacOS: `vcpkg install libvpx libyuv opus aom`
|
||||
- Chạy lệnh `cargo run`
|
||||
|
||||
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Cách để build cho Linux
|
||||
## Cách build cho Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
@@ -78,7 +69,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Cách cài vcpkg
|
||||
### Cách cài đặt `vcpkg`
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
@@ -90,7 +81,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### Cách sửa lỗi libvpx (Dành cho hệ điều hành Fedora)
|
||||
### Cách sửa lỗi `libvpx` (Dành cho hệ điều hành Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
@@ -103,7 +94,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Cách build
|
||||
### Build
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
@@ -116,9 +107,9 @@ mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Cách để build sử dụng Docker
|
||||
## Cách build bằng Docker
|
||||
|
||||
Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer:
|
||||
Bắt đầu bằng cách sao chép repo này về máy tính của bạn và tạo Docker container:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
@@ -126,37 +117,37 @@ cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Rồi mỗi khi bạn chạy ứng dụng, thì hãy chạy lệnh này:
|
||||
Sau đó, mỗi khi bạn chạy ứng dụng, thì hãy chạy dòng lệnh sau:
|
||||
|
||||
```sh
|
||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||
```
|
||||
|
||||
Chú ý: Lần build đầu tiên có thể sẽ mất lâu hơn truớc khi các dependecies đuợc lưu lại, những lần build sau sẽ nhanh hơn. Hơn nũa, nếu bạn cần cung cấp các cài đặt lệnh khác cho lệnh build, bạn có thể đặt những cài đặt lệnh này vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh ‘--release’. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
|
||||
Lưu ý rằng **lần build đầu tiên có thể mất thời gian hơn trước khi các dependencies được lưu vào bộ nhớ cache**, nhưng các lần build sau sẽ nhanh hơn. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên với tùy chọn `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh:
|
||||
Nếu bạn đang chạy bản build được tối ưu hóa, thì bạn có thể chạy với lệnh:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Hãy đảm bảo là bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn.
|
||||
Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác của **cargo** như **install** hoặc **run** hiện không được hỗ trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính của bạn.
|
||||
|
||||
## Cấu trúc tệp tin
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: điều khiển máy tính/chuột trên các nền tảng khác nhau
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed.
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối với một peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành cho điện thoại
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
|
||||
|
||||
## Snapshot
|
||||
|
||||
@@ -30,23 +30,6 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## 免费的公共服务器
|
||||
|
||||
以下是您可以使用的、免费的、会随时更新的公共服务器列表,在国内也许网速会很慢或者无法访问。
|
||||
|
||||
| Location | Vendor | Specification |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
|
||||
|
||||
## Dev Container
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
如果你已经安装了 VS Code 和 Docker, 你可以点击上面的徽章开始使用. 点击后, VS Code 将自动安装 Dev Containers 扩展(如果需要),将源代码克隆到容器卷中, 并启动一个 Dev 容器供使用.
|
||||
|
||||
Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
|
||||
|
||||
## 依赖
|
||||
|
||||
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。
|
||||
|
||||
@@ -8,29 +8,43 @@
|
||||
"modules": [
|
||||
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
|
||||
"xdotool.json",
|
||||
{
|
||||
"name": "pam",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"./configure --disable-selinux --prefix=/app && make -j4 install"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
|
||||
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rustdesk",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"bsdtar -zxvf rustdesk-1.2.4.deb",
|
||||
"bsdtar -zxvf rustdesk.deb",
|
||||
"tar -xvf ./data.tar.xz",
|
||||
"cp -r ./usr/* /app/",
|
||||
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
|
||||
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
|
||||
"mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
|
||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
|
||||
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
|
||||
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png logo.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
|
||||
"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": [
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../rustdesk-1.2.4.deb"
|
||||
"path": "./rustdesk.deb"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../res/logo.svg"
|
||||
"path": "../res/scalable.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
flutter/.gitignore
vendored
1
flutter/.gitignore
vendored
@@ -32,7 +32,6 @@
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
@@ -108,4 +108,3 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "768133699366",
|
||||
"firebase_url": "https://rustdesk.firebaseio.com",
|
||||
"project_id": "rustdesk",
|
||||
"storage_bucket": "rustdesk.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
|
||||
"android_client_info": {
|
||||
"package_name": "com.carriez.flutter_hbb"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package="com.carriez.flutter_hbb">
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import ffi.FFI
|
||||
|
||||
/**
|
||||
* Capture screen,get video and audio,send to rust.
|
||||
* Dispatch notifications
|
||||
@@ -52,7 +54,6 @@ const val NOTIFY_ID_OFFSET = 100
|
||||
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
|
||||
|
||||
// video const
|
||||
const val MAX_SCREEN_SIZE = 1200
|
||||
|
||||
const val VIDEO_KEY_BIT_RATE = 1024_000
|
||||
const val VIDEO_KEY_FRAME_RATE = 30
|
||||
@@ -64,10 +65,6 @@ const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||
|
||||
class MainService : Service() {
|
||||
|
||||
init {
|
||||
System.loadLibrary("rustdesk")
|
||||
}
|
||||
|
||||
@Keep
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
|
||||
@@ -156,23 +153,9 @@ class MainService : Service() {
|
||||
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
||||
|
||||
// jvm call rust
|
||||
private external fun init(ctx: Context)
|
||||
|
||||
/// When app start on boot, app_dir will not be passed from flutter
|
||||
/// so pass a app_dir here to rust server
|
||||
private external fun startServer(app_dir: String)
|
||||
private external fun startService()
|
||||
private external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||
private external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||
private external fun translateLocale(localeName: String, input: String): String
|
||||
private external fun refreshScreen()
|
||||
private external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
// private external fun sendVp9(data: ByteArray)
|
||||
|
||||
private fun translate(input: String): String {
|
||||
Log.d(logTag, "translate:$LOCAL_NAME")
|
||||
return translateLocale(LOCAL_NAME, input)
|
||||
return FFI.translateLocale(LOCAL_NAME, input)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -188,6 +171,7 @@ class MainService : Service() {
|
||||
private val useVP9 = false
|
||||
private val binder = LocalBinder()
|
||||
|
||||
private var reuseVirtualDisplay = Build.VERSION.SDK_INT > 33
|
||||
|
||||
// video
|
||||
private var mediaProjection: MediaProjection? = null
|
||||
@@ -210,8 +194,8 @@ class MainService : Service() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(logTag,"MainService onCreate")
|
||||
init(this)
|
||||
Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay")
|
||||
FFI.init(this)
|
||||
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
serviceLooper = looper
|
||||
@@ -223,7 +207,7 @@ class MainService : Service() {
|
||||
// keep the config dir same with flutter
|
||||
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
||||
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
||||
startServer(configPath)
|
||||
FFI.startServer(configPath, "")
|
||||
|
||||
createForegroundNotification()
|
||||
}
|
||||
@@ -265,12 +249,6 @@ class MainService : Service() {
|
||||
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||
var scale = 1
|
||||
if (w != 0 && h != 0) {
|
||||
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) {
|
||||
scale = 2
|
||||
w /= scale
|
||||
h /= scale
|
||||
dpi /= scale
|
||||
}
|
||||
if (SCREEN_INFO.width != w) {
|
||||
SCREEN_INFO.width = w
|
||||
SCREEN_INFO.height = h
|
||||
@@ -278,7 +256,7 @@ class MainService : Service() {
|
||||
SCREEN_INFO.dpi = dpi
|
||||
if (isStart) {
|
||||
stopCapture()
|
||||
refreshScreen()
|
||||
FFI.refreshScreen()
|
||||
startCapture()
|
||||
}
|
||||
}
|
||||
@@ -306,7 +284,7 @@ class MainService : Service() {
|
||||
createForegroundNotification()
|
||||
|
||||
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
||||
startService()
|
||||
FFI.startService()
|
||||
}
|
||||
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
|
||||
val mediaProjectionManager =
|
||||
@@ -354,12 +332,13 @@ class MainService : Service() {
|
||||
).apply {
|
||||
setOnImageAvailableListener({ imageReader: ImageReader ->
|
||||
try {
|
||||
// If not call acquireLatestImage, listener will not be called again
|
||||
imageReader.acquireLatestImage().use { image ->
|
||||
if (image == null) return@setOnImageAvailableListener
|
||||
if (image == null || !isStart) return@setOnImageAvailableListener
|
||||
val planes = image.planes
|
||||
val buffer = planes[0].buffer
|
||||
buffer.rewind()
|
||||
onVideoFrameUpdate(buffer)
|
||||
FFI.onVideoFrameUpdate(buffer)
|
||||
}
|
||||
} catch (ignored: java.lang.Exception) {
|
||||
}
|
||||
@@ -378,6 +357,7 @@ class MainService : Service() {
|
||||
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||
return false
|
||||
}
|
||||
|
||||
updateScreenInfo(resources.configuration.orientation)
|
||||
Log.d(logTag, "Start Capture")
|
||||
surface = createSurface()
|
||||
@@ -393,34 +373,45 @@ class MainService : Service() {
|
||||
}
|
||||
checkMediaPermission()
|
||||
_isStart = true
|
||||
setFrameRawEnable("video",true)
|
||||
setFrameRawEnable("audio",true)
|
||||
FFI.setFrameRawEnable("video",true)
|
||||
FFI.setFrameRawEnable("audio",true)
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopCapture() {
|
||||
Log.d(logTag, "Stop Capture")
|
||||
setFrameRawEnable("video",false)
|
||||
setFrameRawEnable("audio",false)
|
||||
FFI.setFrameRawEnable("video",false)
|
||||
FFI.setFrameRawEnable("audio",false)
|
||||
_isStart = false
|
||||
// release video
|
||||
virtualDisplay?.release()
|
||||
surface?.release()
|
||||
if (reuseVirtualDisplay) {
|
||||
// The virtual display video projection can be paused by calling `setSurface(null)`.
|
||||
// https://developer.android.com/reference/android/hardware/display/VirtualDisplay.Callback
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/android.hardware.display.virtualdisplay.callback.onpaused?view=net-android-34.0
|
||||
virtualDisplay?.setSurface(null)
|
||||
} else {
|
||||
virtualDisplay?.release()
|
||||
}
|
||||
// suface needs to be release after `imageReader.close()` to imageReader access released surface
|
||||
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
|
||||
imageReader?.close()
|
||||
imageReader = null
|
||||
videoEncoder?.let {
|
||||
it.signalEndOfInputStream()
|
||||
it.stop()
|
||||
it.release()
|
||||
}
|
||||
virtualDisplay = null
|
||||
if (!reuseVirtualDisplay) {
|
||||
virtualDisplay = null
|
||||
}
|
||||
videoEncoder = null
|
||||
// suface needs to be release after `imageReader.close()` to imageReader access released surface
|
||||
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
|
||||
surface?.release()
|
||||
|
||||
// release audio
|
||||
audioRecordStat = false
|
||||
audioRecorder?.release()
|
||||
audioRecorder = null
|
||||
minBufferSize = 0
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
@@ -428,8 +419,11 @@ class MainService : Service() {
|
||||
_isReady = false
|
||||
|
||||
stopCapture()
|
||||
imageReader?.close()
|
||||
imageReader = null
|
||||
|
||||
if (reuseVirtualDisplay) {
|
||||
virtualDisplay?.release()
|
||||
virtualDisplay = null
|
||||
}
|
||||
|
||||
mediaProjection = null
|
||||
checkMediaPermission()
|
||||
@@ -459,11 +453,7 @@ class MainService : Service() {
|
||||
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||
return
|
||||
}
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface, null, null
|
||||
)
|
||||
createOrSetVirtualDisplay(mp, surface!!)
|
||||
}
|
||||
|
||||
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
||||
@@ -475,11 +465,28 @@ class MainService : Service() {
|
||||
}
|
||||
it.setCallback(cb)
|
||||
it.start()
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
surface, null, null
|
||||
)
|
||||
createOrSetVirtualDisplay(mp, surface!!)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
||||
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
||||
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
|
||||
try {
|
||||
virtualDisplay?.let {
|
||||
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
||||
it.setSurface(s)
|
||||
} ?: let {
|
||||
virtualDisplay = mp.createVirtualDisplay(
|
||||
"RustDeskVD",
|
||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
s, null, null
|
||||
)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
|
||||
// This initiates a prompt dialog for the user to confirm screen projection.
|
||||
requestMediaProjection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,9 +544,13 @@ class MainService : Service() {
|
||||
thread {
|
||||
while (audioRecordStat) {
|
||||
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||
onAudioFrameUpdate(it)
|
||||
FFI.onAudioFrameUpdate(it)
|
||||
}
|
||||
}
|
||||
// let's release here rather than onDestroy to avoid threading issue
|
||||
audioRecorder?.release()
|
||||
audioRecorder = null
|
||||
minBufferSize = 0
|
||||
Log.d(logTag, "Exit audio thread")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
21
flutter/android/app/src/main/kotlin/ffi.kt
Normal file
21
flutter/android/app/src/main/kotlin/ffi.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
// ffi.kt
|
||||
|
||||
package ffi
|
||||
|
||||
import android.content.Context
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object FFI {
|
||||
init {
|
||||
System.loadLibrary("rustdesk")
|
||||
}
|
||||
|
||||
external fun init(ctx: Context)
|
||||
external fun startServer(app_dir: String, custom_client_config: String)
|
||||
external fun startService()
|
||||
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||
external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||
external fun translateLocale(localeName: String, input: String): String
|
||||
external fun refreshScreen()
|
||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="-186 -186 1365 1365"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -139,4 +139,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi' hide Size;
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:back_button_interceptor/back_button_interceptor.dart';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -25,7 +22,6 @@ import 'package:get/get.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:win32/win32.dart' as win32;
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:window_size/window_size.dart' as window_size;
|
||||
|
||||
@@ -33,22 +29,38 @@ import '../consts.dart';
|
||||
import 'common/widgets/overlay.dart';
|
||||
import 'mobile/pages/file_manager_page.dart';
|
||||
import 'mobile/pages/remote_page.dart';
|
||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'models/input_model.dart';
|
||||
import 'models/model.dart';
|
||||
import 'models/platform_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/native/win32.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
|
||||
import 'package:flutter_hbb/native/common.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/common.dart';
|
||||
|
||||
final globalKey = GlobalKey<NavigatorState>();
|
||||
final navigationBarKey = GlobalKey();
|
||||
|
||||
final isAndroid = Platform.isAndroid;
|
||||
final isIOS = Platform.isIOS;
|
||||
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
var isWeb = false;
|
||||
var isWebDesktop = false;
|
||||
final isAndroid = isAndroid_;
|
||||
final isIOS = isIOS_;
|
||||
final isWindows = isWindows_;
|
||||
final isMacOS = isMacOS_;
|
||||
final isLinux = isLinux_;
|
||||
final isDesktop = isDesktop_;
|
||||
final isWeb = isWeb_;
|
||||
final isWebDesktop = isWebDesktop_;
|
||||
var isMobile = isAndroid || isIOS;
|
||||
var version = "";
|
||||
var version = '';
|
||||
int androidVersion = 0;
|
||||
|
||||
// Only used on Linux.
|
||||
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux.
|
||||
// https://stackoverflow.com/questions/8193613/gtk-window-resize-disable-without-going-back-to-default
|
||||
// So we need to use this flag to enable/disable resizable.
|
||||
bool _linuxWindowResizable = true;
|
||||
|
||||
/// only available for Windows target
|
||||
int windowsBuildNumber = 0;
|
||||
DesktopType? desktopType;
|
||||
@@ -56,6 +68,8 @@ DesktopType? desktopType;
|
||||
bool get isMainDesktopWindow =>
|
||||
desktopType == DesktopType.main || desktopType == DesktopType.cm;
|
||||
|
||||
String get screenInfo => screenInfo_;
|
||||
|
||||
/// Check if the app is running with single view mode.
|
||||
bool isSingleViewApp() {
|
||||
return desktopType == DesktopType.cm;
|
||||
@@ -229,11 +243,13 @@ class MyTheme {
|
||||
);
|
||||
|
||||
static SwitchThemeData switchTheme() {
|
||||
return SwitchThemeData(splashRadius: isDesktop ? 0 : kRadialReactionRadius);
|
||||
return SwitchThemeData(
|
||||
splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
|
||||
}
|
||||
|
||||
static RadioThemeData radioTheme() {
|
||||
return RadioThemeData(splashRadius: isDesktop ? 0 : kRadialReactionRadius);
|
||||
return RadioThemeData(
|
||||
splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
|
||||
}
|
||||
|
||||
// Checkbox
|
||||
@@ -282,7 +298,7 @@ class MyTheme {
|
||||
static EdgeInsets dialogContentPadding({bool actions = true}) {
|
||||
final double p = dialogPadding;
|
||||
|
||||
return isDesktop
|
||||
return (isDesktop || isWebDesktop)
|
||||
? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p)
|
||||
: EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p);
|
||||
}
|
||||
@@ -290,12 +306,12 @@ class MyTheme {
|
||||
static EdgeInsets dialogActionsPadding() {
|
||||
final double p = dialogPadding;
|
||||
|
||||
return isDesktop
|
||||
return (isDesktop || isWebDesktop)
|
||||
? EdgeInsets.fromLTRB(p, 0, p, (p - 4))
|
||||
: EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2));
|
||||
}
|
||||
|
||||
static EdgeInsets dialogButtonPadding = isDesktop
|
||||
static EdgeInsets dialogButtonPadding = (isDesktop || isWebDesktop)
|
||||
? EdgeInsets.only(left: dialogPadding)
|
||||
: EdgeInsets.only(left: dialogPadding / 3);
|
||||
|
||||
@@ -367,10 +383,10 @@ class MyTheme {
|
||||
labelColor: Colors.black87,
|
||||
),
|
||||
tooltipTheme: tooltipTheme(),
|
||||
splashColor: isDesktop ? Colors.transparent : null,
|
||||
highlightColor: isDesktop ? Colors.transparent : null,
|
||||
splashFactory: isDesktop ? NoSplash.splashFactory : null,
|
||||
textButtonTheme: isDesktop
|
||||
splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
|
||||
highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
|
||||
splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
|
||||
textButtonTheme: (isDesktop || isWebDesktop)
|
||||
? TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
@@ -410,7 +426,9 @@ class MyTheme {
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: isDesktop ? Color(0xFFECECEC) : Colors.transparent),
|
||||
color: (isDesktop || isWebDesktop)
|
||||
? Color(0xFFECECEC)
|
||||
: Colors.transparent),
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
)),
|
||||
).copyWith(
|
||||
@@ -436,7 +454,7 @@ class MyTheme {
|
||||
),
|
||||
),
|
||||
scrollbarTheme: scrollbarThemeDark,
|
||||
inputDecorationTheme: isDesktop
|
||||
inputDecorationTheme: (isDesktop || isWebDesktop)
|
||||
? InputDecorationTheme(
|
||||
fillColor: Color(0xFF24252B),
|
||||
filled: true,
|
||||
@@ -463,10 +481,10 @@ class MyTheme {
|
||||
labelColor: Colors.white70,
|
||||
),
|
||||
tooltipTheme: tooltipTheme(),
|
||||
splashColor: isDesktop ? Colors.transparent : null,
|
||||
highlightColor: isDesktop ? Colors.transparent : null,
|
||||
splashFactory: isDesktop ? NoSplash.splashFactory : null,
|
||||
textButtonTheme: isDesktop
|
||||
splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
|
||||
highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
|
||||
splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
|
||||
textButtonTheme: (isDesktop || isWebDesktop)
|
||||
? TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
@@ -631,8 +649,12 @@ closeConnection({String? id}) {
|
||||
gFFI.chatModel.hideChatOverlay();
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
} else {
|
||||
final controller = Get.find<DesktopTabController>();
|
||||
controller.closeBy(id);
|
||||
if (isWeb) {
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
} else {
|
||||
final controller = Get.find<DesktopTabController>();
|
||||
controller.closeBy(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,7 +832,7 @@ class OverlayDialogManager {
|
||||
Offstage(
|
||||
offstage: !showCancel,
|
||||
child: Center(
|
||||
child: isDesktop
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? dialogButton('Cancel', onPressed: cancel)
|
||||
: TextButton(
|
||||
style: flatButtonStyle,
|
||||
@@ -1285,7 +1307,7 @@ class AndroidPermissionManager {
|
||||
}
|
||||
|
||||
static Future<bool> check(String type) {
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWeb) {
|
||||
return Future.value(true);
|
||||
}
|
||||
return gFFI.invokeMethod("check_permission", type);
|
||||
@@ -1299,7 +1321,7 @@ class AndroidPermissionManager {
|
||||
/// We use XXPermissions to request permissions,
|
||||
/// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
|
||||
static Future<bool> request(String type) {
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWeb) {
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
@@ -1521,6 +1543,12 @@ class LastWindowPosition {
|
||||
}
|
||||
}
|
||||
|
||||
String get windowFramePrefix =>
|
||||
kWindowPrefix +
|
||||
(bind.isIncomingOnly()
|
||||
? "incoming_"
|
||||
: (bind.isOutgoingOnly() ? "outgoing_" : ""));
|
||||
|
||||
/// Save window position and size on exit
|
||||
/// Note that windowId must be provided if it's subwindow
|
||||
Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
@@ -1533,10 +1561,10 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
late Size sz;
|
||||
late bool isMaximized;
|
||||
bool isFullscreen = stateGlobal.fullscreen.isTrue ||
|
||||
(Platform.isMacOS && stateGlobal.closeOnFullscreen == true);
|
||||
(isMacOS && stateGlobal.closeOnFullscreen == true);
|
||||
setFrameIfMaximized() {
|
||||
if (isMaximized) {
|
||||
final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
|
||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
position = Offset(
|
||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||
@@ -1546,7 +1574,13 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
|
||||
switch (type) {
|
||||
case WindowType.Main:
|
||||
isMaximized = await windowManager.isMaximized();
|
||||
// Checking `bind.isIncomingOnly()` is a simple workaround for MacOS.
|
||||
// `await windowManager.isMaximized()` will always return true
|
||||
// if is not resizable. The reason is unknown.
|
||||
//
|
||||
// `setResizable(!bind.isIncomingOnly());` in main.dart
|
||||
isMaximized =
|
||||
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
|
||||
position = await windowManager.getPosition();
|
||||
sz = await windowManager.getSize();
|
||||
setFrameIfMaximized();
|
||||
@@ -1566,7 +1600,7 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
setFrameIfMaximized();
|
||||
break;
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
const kMinOffset = -10000;
|
||||
const kMaxOffset = 10000;
|
||||
if (position.dx < kMinOffset ||
|
||||
@@ -1584,7 +1618,7 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
||||
"Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
|
||||
|
||||
await bind.setLocalFlutterOption(
|
||||
k: kWindowPrefix + type.name, v: pos.toString());
|
||||
k: windowFramePrefix + type.name, v: pos.toString());
|
||||
|
||||
if (type == WindowType.RemoteDesktop && windowId != null) {
|
||||
await _saveSessionWindowPosition(
|
||||
@@ -1599,7 +1633,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
|
||||
getPeerPos(String peerId) {
|
||||
if (isMaximized) {
|
||||
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||
id: peerId, k: kWindowPrefix + windowType.name);
|
||||
id: peerId, k: windowFramePrefix + windowType.name);
|
||||
var lpos = LastWindowPosition.loadFromString(peerPos);
|
||||
return LastWindowPosition(
|
||||
lpos?.width ?? pos.offsetWidth,
|
||||
@@ -1618,7 +1652,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
|
||||
for (final peerId in remoteList.split(',')) {
|
||||
bind.mainSetPeerFlutterOptionSync(
|
||||
id: peerId,
|
||||
k: kWindowPrefix + windowType.name,
|
||||
k: windowFramePrefix + windowType.name,
|
||||
v: getPeerPos(peerId));
|
||||
}
|
||||
}
|
||||
@@ -1736,14 +1770,14 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
// Because the session may not be read at this time.
|
||||
if (desktopType == DesktopType.main) {
|
||||
pos = bind.mainGetPeerFlutterOptionSync(
|
||||
id: peerId, k: kWindowPrefix + type.name);
|
||||
id: peerId, k: windowFramePrefix + type.name);
|
||||
} else {
|
||||
pos = await bind.sessionGetFlutterOptionByPeerId(
|
||||
id: peerId, k: kWindowPrefix + type.name);
|
||||
id: peerId, k: windowFramePrefix + type.name);
|
||||
}
|
||||
isRemotePeerPos = pos != null;
|
||||
}
|
||||
pos ??= bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
|
||||
pos ??= bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||
|
||||
var lpos = LastWindowPosition.loadFromString(pos);
|
||||
if (lpos == null) {
|
||||
@@ -1790,9 +1824,13 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
}
|
||||
if (lpos.isMaximized == true) {
|
||||
await restorePos();
|
||||
await windowManager.maximize();
|
||||
if (!(bind.isIncomingOnly() || bind.isOutgoingOnly())) {
|
||||
await windowManager.maximize();
|
||||
}
|
||||
} else {
|
||||
await windowManager.setSize(size);
|
||||
if (!bind.isIncomingOnly() || bind.isOutgoingOnly()) {
|
||||
await windowManager.setSize(size);
|
||||
}
|
||||
await restorePos();
|
||||
}
|
||||
return true;
|
||||
@@ -1833,13 +1871,14 @@ Future<bool> restoreWindowPosition(WindowType type,
|
||||
/// initUniLinks should only be used on macos/windows.
|
||||
/// we use dbus for linux currently.
|
||||
Future<bool> initUniLinks() async {
|
||||
if (Platform.isLinux) {
|
||||
if (isLinux) {
|
||||
return false;
|
||||
}
|
||||
// check cold boot
|
||||
try {
|
||||
final initialLink = await getInitialLink();
|
||||
if (initialLink == null) {
|
||||
print("initialLink: $initialLink");
|
||||
if (initialLink == null || initialLink.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return handleUriLink(uriString: initialLink);
|
||||
@@ -1855,7 +1894,7 @@ Future<bool> initUniLinks() async {
|
||||
///
|
||||
/// Returns a [StreamSubscription] which can listen the uni links.
|
||||
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
|
||||
if (Platform.isLinux) {
|
||||
if (isLinux) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1889,7 +1928,7 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
if (cmdArgs != null && cmdArgs.isNotEmpty) {
|
||||
args = cmdArgs;
|
||||
// rustdesk <uri link>
|
||||
if (args[0].startsWith(kUniLinksPrefix)) {
|
||||
if (args[0].startsWith(bind.mainUriPrefixSync())) {
|
||||
final uri = Uri.tryParse(args[0]);
|
||||
if (uri != null) {
|
||||
args = urlLinkToCmdArgs(uri);
|
||||
@@ -2003,7 +2042,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
command = '--connect';
|
||||
id = uri.path.substring("/new/".length);
|
||||
} else if (uri.authority == "config") {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
if (isAndroid || isIOS) {
|
||||
final config = uri.path.substring("/".length);
|
||||
// add a timer to make showToast work
|
||||
Timer(Duration(seconds: 1), () {
|
||||
@@ -2012,7 +2051,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
}
|
||||
return null;
|
||||
} else if (uri.authority == "password") {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
if (isAndroid || isIOS) {
|
||||
final password = uri.path.substring("/".length);
|
||||
if (password.isNotEmpty) {
|
||||
Timer(Duration(seconds: 1), () async {
|
||||
@@ -2079,19 +2118,28 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
connectMainDesktop(
|
||||
String id, {
|
||||
required bool isFileTransfer,
|
||||
required bool isTcpTunneling,
|
||||
required bool isRDP,
|
||||
bool? forceRelay,
|
||||
}) async {
|
||||
connectMainDesktop(String id,
|
||||
{required bool isFileTransfer,
|
||||
required bool isTcpTunneling,
|
||||
required bool isRDP,
|
||||
bool? forceRelay,
|
||||
String? password,
|
||||
bool? isSharedPassword}) async {
|
||||
if (isFileTransfer) {
|
||||
await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
|
||||
await rustDeskWinManager.newFileTransfer(id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isTcpTunneling || isRDP) {
|
||||
await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay);
|
||||
await rustDeskWinManager.newPortForward(id, isRDP,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay);
|
||||
} else {
|
||||
await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay);
|
||||
await rustDeskWinManager.newRemoteDesktop(id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2099,14 +2147,13 @@ connectMainDesktop(
|
||||
/// If [isFileTransfer], starts a session only for file transfer.
|
||||
/// If [isTcpTunneling], starts a session only for tcp tunneling.
|
||||
/// If [isRDP], starts a session only for rdp.
|
||||
connect(
|
||||
BuildContext context,
|
||||
String id, {
|
||||
bool isFileTransfer = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false,
|
||||
}) async {
|
||||
connect(BuildContext context, String id,
|
||||
{bool isFileTransfer = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false,
|
||||
String? password,
|
||||
bool? isSharedPassword}) async {
|
||||
if (id == '') return;
|
||||
if (!isDesktop || desktopType == DesktopType.main) {
|
||||
try {
|
||||
@@ -2134,6 +2181,8 @@ connect(
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay2,
|
||||
);
|
||||
} else {
|
||||
@@ -2142,6 +2191,8 @@ connect(
|
||||
'isFileTransfer': isFileTransfer,
|
||||
'isTcpTunneling': isTcpTunneling,
|
||||
'isRDP': isRDP,
|
||||
'password': password,
|
||||
'isSharedPassword': isSharedPassword,
|
||||
'forceRelay': forceRelay,
|
||||
});
|
||||
}
|
||||
@@ -2155,16 +2206,34 @@ connect(
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => FileManagerPage(id: id),
|
||||
builder: (BuildContext context) => FileManagerPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => RemotePage(id: id),
|
||||
),
|
||||
);
|
||||
if (isWebDesktop) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => desktop_remote.RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => RemotePage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2218,7 +2287,7 @@ Future<void> reloadAllWindows() async {
|
||||
/// [Note]
|
||||
/// Portable build is only available on Windows.
|
||||
bool isRunningInPortableMode() {
|
||||
if (!Platform.isWindows) {
|
||||
if (!isWindows) {
|
||||
return false;
|
||||
}
|
||||
return bool.hasEnvironment(kEnvPortableExecutable);
|
||||
@@ -2231,7 +2300,7 @@ Future<void> onActiveWindowChanged() async {
|
||||
if (rustDeskWinManager.getActiveWindows().isEmpty) {
|
||||
// close all sub windows
|
||||
try {
|
||||
if (Platform.isLinux) {
|
||||
if (isLinux) {
|
||||
await Future.wait([
|
||||
saveWindowPosition(WindowType.Main),
|
||||
rustDeskWinManager.closeAllSubWindows()
|
||||
@@ -2245,7 +2314,7 @@ Future<void> onActiveWindowChanged() async {
|
||||
debugPrint("Start closing RustDesk...");
|
||||
await windowManager.setPreventClose(false);
|
||||
await windowManager.close();
|
||||
if (Platform.isMacOS) {
|
||||
if (isMacOS) {
|
||||
RdPlatformChannel.instance.terminate();
|
||||
}
|
||||
}
|
||||
@@ -2261,7 +2330,7 @@ Timer periodic_immediate(Duration duration, Future<void> Function() callback) {
|
||||
|
||||
/// return a human readable windows version
|
||||
WindowsTarget getWindowsTarget(int buildNumber) {
|
||||
if (!Platform.isWindows) {
|
||||
if (!isWindows) {
|
||||
return WindowsTarget.naw;
|
||||
}
|
||||
if (buildNumber >= 22000) {
|
||||
@@ -2287,35 +2356,7 @@ WindowsTarget getWindowsTarget(int buildNumber) {
|
||||
/// [Note]
|
||||
/// Please use this function wrapped with `Platform.isWindows`.
|
||||
int getWindowsTargetBuildNumber() {
|
||||
final rtlGetVersion = DynamicLibrary.open('ntdll.dll').lookupFunction<
|
||||
Void Function(Pointer<win32.OSVERSIONINFOEX>),
|
||||
void Function(Pointer<win32.OSVERSIONINFOEX>)>('RtlGetVersion');
|
||||
final osVersionInfo = getOSVERSIONINFOEXPointer();
|
||||
rtlGetVersion(osVersionInfo);
|
||||
int buildNumber = osVersionInfo.ref.dwBuildNumber;
|
||||
calloc.free(osVersionInfo);
|
||||
return buildNumber;
|
||||
}
|
||||
|
||||
/// Get Windows OS version pointer
|
||||
///
|
||||
/// [Note]
|
||||
/// Please use this function wrapped with `Platform.isWindows`.
|
||||
Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
|
||||
final pointer = calloc<win32.OSVERSIONINFOEX>();
|
||||
pointer.ref
|
||||
..dwOSVersionInfoSize = sizeOf<win32.OSVERSIONINFOEX>()
|
||||
..dwBuildNumber = 0
|
||||
..dwMajorVersion = 0
|
||||
..dwMinorVersion = 0
|
||||
..dwPlatformId = 0
|
||||
..szCSDVersion = ''
|
||||
..wServicePackMajor = 0
|
||||
..wServicePackMinor = 0
|
||||
..wSuiteMask = 0
|
||||
..wProductType = 0
|
||||
..wReserved = 0;
|
||||
return pointer;
|
||||
return getWindowsTargetBuildNumber_();
|
||||
}
|
||||
|
||||
/// Indicating we need to use compatible ui mode.
|
||||
@@ -2323,7 +2364,7 @@ Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
|
||||
/// [Conditions]
|
||||
/// - Windows 7, window will overflow when we use frameless ui.
|
||||
bool get kUseCompatibleUiMode =>
|
||||
Platform.isWindows &&
|
||||
isWindows &&
|
||||
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
|
||||
|
||||
class ServerConfig {
|
||||
@@ -2387,7 +2428,7 @@ Widget dialogButton(String text,
|
||||
Widget? icon,
|
||||
TextStyle? style,
|
||||
ButtonStyle? buttonStyle}) {
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
if (isOutline) {
|
||||
return icon == null
|
||||
? OutlinedButton(
|
||||
@@ -2429,19 +2470,20 @@ int versionCmp(String v1, String v2) {
|
||||
}
|
||||
|
||||
String getWindowName({WindowType? overrideType}) {
|
||||
final name = bind.mainGetAppNameSync();
|
||||
switch (overrideType ?? kWindowType) {
|
||||
case WindowType.Main:
|
||||
return "RustDesk";
|
||||
return name;
|
||||
case WindowType.FileTransfer:
|
||||
return "File Transfer - RustDesk";
|
||||
return "File Transfer - $name";
|
||||
case WindowType.PortForward:
|
||||
return "Port Forward - RustDesk";
|
||||
return "Port Forward - $name";
|
||||
case WindowType.RemoteDesktop:
|
||||
return "Remote Desktop - RustDesk";
|
||||
return "Remote Desktop - $name";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "RustDesk";
|
||||
return name;
|
||||
}
|
||||
|
||||
String getWindowNameWithId(String id, {WindowType? overrideType}) {
|
||||
@@ -2452,7 +2494,7 @@ Future<void> updateSystemWindowTheme() async {
|
||||
// Set system window theme for macOS.
|
||||
final userPreference = MyTheme.getThemeModePreference();
|
||||
if (userPreference != ThemeMode.system) {
|
||||
if (Platform.isMacOS) {
|
||||
if (isMacOS) {
|
||||
await RdPlatformChannel.instance.changeSystemWindowTheme(
|
||||
userPreference == ThemeMode.light
|
||||
? SystemWindowTheme.light
|
||||
@@ -2540,7 +2582,7 @@ void onCopyFingerprint(String value) {
|
||||
|
||||
Future<bool> callMainCheckSuperUserPermission() async {
|
||||
bool checked = await bind.mainCheckSuperUserPermission();
|
||||
if (Platform.isMacOS) {
|
||||
if (isMacOS) {
|
||||
await windowManager.show();
|
||||
}
|
||||
return checked;
|
||||
@@ -2548,7 +2590,7 @@ Future<bool> callMainCheckSuperUserPermission() async {
|
||||
|
||||
Future<void> start_service(bool is_start) async {
|
||||
bool checked = !bind.mainIsInstalled() ||
|
||||
!Platform.isMacOS ||
|
||||
!isMacOS ||
|
||||
await callMainCheckSuperUserPermission();
|
||||
if (checked) {
|
||||
bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
|
||||
@@ -2942,16 +2984,16 @@ Future<bool> setServerConfig(
|
||||
}
|
||||
// id
|
||||
if (config.idServer.isNotEmpty && errMsgs != null) {
|
||||
errMsgs[0].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.idServer));
|
||||
errMsgs[0].value = translate(await bind.mainTestIfValidServer(
|
||||
server: config.idServer, testWithProxy: true));
|
||||
if (errMsgs[0].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// relay
|
||||
if (config.relayServer.isNotEmpty && errMsgs != null) {
|
||||
errMsgs[1].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.relayServer));
|
||||
errMsgs[1].value = translate(await bind.mainTestIfValidServer(
|
||||
server: config.relayServer, testWithProxy: true));
|
||||
if (errMsgs[1].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
@@ -2973,7 +3015,6 @@ Future<bool> setServerConfig(
|
||||
await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
|
||||
await bind.mainSetOption(key: 'api-server', value: config.apiServer);
|
||||
await bind.mainSetOption(key: 'key', value: config.key);
|
||||
|
||||
final newApiServer = await bind.mainGetApiServer();
|
||||
if (oldApiServer.isNotEmpty &&
|
||||
oldApiServer != newApiServer &&
|
||||
@@ -3070,3 +3111,173 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
|
||||
? null
|
||||
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
|
||||
}
|
||||
|
||||
Widget loadPowered(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://rustdesk.com'));
|
||||
},
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Text(
|
||||
translate("powered_by_me"),
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(fontSize: 9, decoration: TextDecoration.underline),
|
||||
)),
|
||||
),
|
||||
).marginOnly(top: 6);
|
||||
}
|
||||
|
||||
// max 300 x 60
|
||||
Widget loadLogo() {
|
||||
return FutureBuilder<ByteData>(
|
||||
future: rootBundle.load('assets/logo.png'),
|
||||
builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final image = Image.asset(
|
||||
'assets/logo.png',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (ctx, error, stackTrace) {
|
||||
return Container();
|
||||
},
|
||||
);
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 300, maxHeight: 60),
|
||||
child: image,
|
||||
).marginOnly(left: 12, right: 12, top: 12);
|
||||
}
|
||||
return const Offstage();
|
||||
});
|
||||
}
|
||||
|
||||
Widget loadIcon(double size) {
|
||||
return Image.asset('assets/icon.png',
|
||||
width: size,
|
||||
height: size,
|
||||
errorBuilder: (ctx, error, stackTrace) => SvgPicture.asset(
|
||||
'assets/icon.svg',
|
||||
width: size,
|
||||
height: size,
|
||||
));
|
||||
}
|
||||
|
||||
var imcomingOnlyHomeSize = Size(280, 300);
|
||||
Size getIncomingOnlyHomeSize() {
|
||||
final magicWidth = isWindows ? 11.0 : 2.0;
|
||||
final magicHeight = 10.0;
|
||||
return imcomingOnlyHomeSize +
|
||||
Offset(magicWidth, kDesktopRemoteTabBarHeight + magicHeight);
|
||||
}
|
||||
|
||||
Size getIncomingOnlySettingsSize() {
|
||||
return Size(768, 600);
|
||||
}
|
||||
|
||||
bool isInHomePage() {
|
||||
final controller = Get.find<DesktopTabController>();
|
||||
return controller.state.value.selected == 0;
|
||||
}
|
||||
|
||||
Widget buildPresetPasswordWarning() {
|
||||
return FutureBuilder<bool>(
|
||||
future: bind.isPresetPassword(),
|
||||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(
|
||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||
} else if (snapshot.hasData && snapshot.data == true) {
|
||||
return Container(
|
||||
color: Colors.yellow,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
child: Text(
|
||||
translate("Security Alert"),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
translate("preset_password_warning"),
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
); // Show a warning message if the Future completed with true
|
||||
} else {
|
||||
return SizedBox
|
||||
.shrink(); // Show nothing if the Future completed with false or null
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
|
||||
Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
|
||||
boxShadow() => isMainDesktopWindow
|
||||
? <BoxShadow>[
|
||||
if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse)
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
offset: Offset(
|
||||
0.0,
|
||||
stateGlobal.isFocused.isTrue
|
||||
? kFrameBoxShadowOffsetFocused
|
||||
: kFrameBoxShadowOffsetUnfocused),
|
||||
blurRadius: kFrameBoxShadowBlurRadius,
|
||||
),
|
||||
]
|
||||
: null;
|
||||
return Obx(
|
||||
() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isMainDesktopWindow
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.background,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: stateGlobal.windowBorderWidth.value,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
|
||||
? 0
|
||||
: kFrameBorderRadius,
|
||||
),
|
||||
boxShadow: boxShadow(),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
|
||||
? 0
|
||||
: kFrameClipRRectBorderRadius,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
|
||||
|
||||
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
|
||||
// See _linuxWindowResizable for more details.
|
||||
// So we use `setResizable()` instead of `windowManager.setResizable()`.
|
||||
//
|
||||
// We can only call `windowManager.setResizable(false)` if we need the default size on Linux.
|
||||
setResizable(bool resizable) {
|
||||
if (isLinux) {
|
||||
_linuxWindowResizable = resizable;
|
||||
stateGlobal.refreshResizeEdgeSize();
|
||||
} else {
|
||||
windowManager.setResizable(resizable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
@@ -188,3 +189,79 @@ class RequestException implements Exception {
|
||||
return "RequestException, statusCode: $statusCode, error: $cause";
|
||||
}
|
||||
}
|
||||
|
||||
enum ShareRule {
|
||||
read(1),
|
||||
readWrite(2),
|
||||
fullControl(3);
|
||||
|
||||
const ShareRule(this.value);
|
||||
final int value;
|
||||
|
||||
static String desc(int v) {
|
||||
if (v == ShareRule.read.value) {
|
||||
return translate('Read-only');
|
||||
}
|
||||
if (v == ShareRule.readWrite.value) {
|
||||
return translate('Read/Write');
|
||||
}
|
||||
if (v == ShareRule.fullControl.value) {
|
||||
return translate('Full Control');
|
||||
}
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
static String shortDesc(int v) {
|
||||
if (v == ShareRule.read.value) {
|
||||
return 'R';
|
||||
}
|
||||
if (v == ShareRule.readWrite.value) {
|
||||
return 'RW';
|
||||
}
|
||||
if (v == ShareRule.fullControl.value) {
|
||||
return 'F';
|
||||
}
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
static ShareRule? fromValue(int v) {
|
||||
if (v == ShareRule.read.value) {
|
||||
return ShareRule.read;
|
||||
}
|
||||
if (v == ShareRule.readWrite.value) {
|
||||
return ShareRule.readWrite;
|
||||
}
|
||||
if (v == ShareRule.fullControl.value) {
|
||||
return ShareRule.fullControl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class AbProfile {
|
||||
String guid;
|
||||
String name;
|
||||
String owner;
|
||||
String? note;
|
||||
int rule;
|
||||
|
||||
AbProfile(this.guid, this.name, this.owner, this.note, this.rule);
|
||||
|
||||
AbProfile.fromJson(Map<String, dynamic> json)
|
||||
: guid = json['guid'] ?? '',
|
||||
name = json['name'] ?? '',
|
||||
owner = json['owner'] ?? '',
|
||||
note = json['note'] ?? '',
|
||||
rule = json['rule'] ?? 0;
|
||||
}
|
||||
|
||||
class AbTag {
|
||||
String name;
|
||||
int color;
|
||||
|
||||
AbTag(this.name, this.color);
|
||||
|
||||
AbTag.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'] ?? '',
|
||||
color = json['color'] ?? '';
|
||||
}
|
||||
|
||||
@@ -168,6 +168,29 @@ class ShowRemoteCursorState {
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
class ShowRemoteCursorLockState {
|
||||
static String tag(String id) => 'show_remote_cursor_lock_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
class KeyboardEnabledState {
|
||||
static String tag(String id) => 'keyboard_enabled_$id';
|
||||
|
||||
@@ -315,6 +338,7 @@ initSharedStates(String id) {
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
ShowRemoteCursorLockState.init(id);
|
||||
RemoteCursorMovedState.init(id);
|
||||
FingerprintState.init(id);
|
||||
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
||||
@@ -327,6 +351,7 @@ removeSharedStates(String id) {
|
||||
BlockInputState.delete(id);
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
ShowRemoteCursorLockState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
RemoteCursorMovedState.delete(id);
|
||||
FingerprintState.delete(id);
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||
@@ -43,27 +46,24 @@ class _AddressBookState extends State<AddressBook> {
|
||||
child: ElevatedButton(
|
||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||
} else {
|
||||
if (gFFI.abModel.abLoading.value && gFFI.abModel.emtpy) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
if (gFFI.abModel.retrying.value) LinearProgressIndicator(),
|
||||
if (gFFI.abModel.currentAbLoading.value &&
|
||||
gFFI.abModel.currentAbEmpty)
|
||||
const LinearProgressIndicator(),
|
||||
buildErrorBanner(context,
|
||||
loading: gFFI.abModel.abLoading,
|
||||
err: gFFI.abModel.pullError,
|
||||
loading: gFFI.abModel.currentAbLoading,
|
||||
err: gFFI.abModel.currentAbPullError,
|
||||
retry: null,
|
||||
close: () => gFFI.abModel.pullError.value = ''),
|
||||
close: () => gFFI.abModel.currentAbPullError.value = ''),
|
||||
buildErrorBanner(context,
|
||||
loading: gFFI.abModel.abLoading,
|
||||
err: gFFI.abModel.pushError,
|
||||
retry: () => gFFI.abModel.pushAb(isRetry: true),
|
||||
close: () => gFFI.abModel.pushError.value = ''),
|
||||
loading: gFFI.abModel.currentAbLoading,
|
||||
err: gFFI.abModel.currentAbPushError,
|
||||
retry: null, // remove retry
|
||||
close: () => gFFI.abModel.currentAbPushError.value = ''),
|
||||
Expanded(
|
||||
child: isDesktop
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? _buildAddressBookDesktop()
|
||||
: _buildAddressBookMobile())
|
||||
],
|
||||
@@ -82,19 +82,23 @@ class _AddressBookState extends State<AddressBook> {
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.background)),
|
||||
child: Container(
|
||||
width: 150,
|
||||
width: 200,
|
||||
height: double.infinity,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTagHeader().marginOnly(left: 8.0, right: 0),
|
||||
_buildAbDropdown(),
|
||||
_buildTagHeader().marginOnly(
|
||||
left: 8.0,
|
||||
right: gFFI.abModel.legacyMode.value ? 8.0 : 0,
|
||||
top: gFFI.abModel.legacyMode.value ? 8.0 : 0),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: _buildTags(),
|
||||
),
|
||||
)
|
||||
),
|
||||
_buildAbPermission(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -119,11 +123,13 @@ class _AddressBookState extends State<AddressBook> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildAbDropdown(),
|
||||
_buildTagHeader().marginOnly(left: 8.0, right: 0),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: _buildTags(),
|
||||
),
|
||||
_buildAbPermission(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -133,6 +139,131 @@ class _AddressBookState extends State<AddressBook> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAbPermission() {
|
||||
icon(IconData data, String tooltip) {
|
||||
return Tooltip(
|
||||
message: translate(tooltip),
|
||||
waitDuration: Duration.zero,
|
||||
child: Icon(data, size: 12.0).marginSymmetric(horizontal: 2.0));
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
if (gFFI.abModel.legacyMode.value) return Offstage();
|
||||
if (gFFI.abModel.current.isPersonal()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
icon(Icons.cloud_off, "Personal"),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
List<Widget> children = [];
|
||||
final rule = gFFI.abModel.current.sharedProfile()?.rule;
|
||||
if (rule == ShareRule.read.value) {
|
||||
children.add(
|
||||
icon(Icons.visibility, ShareRule.desc(ShareRule.read.value)));
|
||||
} else if (rule == ShareRule.readWrite.value) {
|
||||
children
|
||||
.add(icon(Icons.edit, ShareRule.desc(ShareRule.readWrite.value)));
|
||||
} else if (rule == ShareRule.fullControl.value) {
|
||||
children.add(icon(
|
||||
Icons.security, ShareRule.desc(ShareRule.fullControl.value)));
|
||||
}
|
||||
final owner = gFFI.abModel.current.sharedProfile()?.owner;
|
||||
if (owner != null) {
|
||||
children.add(icon(Icons.person, "${translate("Owner")}: $owner"));
|
||||
}
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildAbDropdown() {
|
||||
if (gFFI.abModel.legacyMode.value) {
|
||||
return Offstage();
|
||||
}
|
||||
final names = gFFI.abModel.addressBookNames();
|
||||
if (!names.contains(gFFI.abModel.currentName.value)) {
|
||||
return Offstage();
|
||||
}
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
|
||||
return DropdownButton2<String>(
|
||||
value: gFFI.abModel.currentName.value,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
gFFI.abModel.setCurrentName(value);
|
||||
bind.setLocalFlutterOption(k: 'current-ab-name', v: value);
|
||||
}
|
||||
},
|
||||
underline: Container(
|
||||
height: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
buttonStyleData: ButtonStyleData(height: 48),
|
||||
menuItemStyleData: MenuItemStyleData(height: 36),
|
||||
items: names
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: gFFI.abModel.translatedName(e),
|
||||
child: Text(
|
||||
gFFI.abModel.translatedName(e),
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
),
|
||||
],
|
||||
)))
|
||||
.toList(),
|
||||
isExpanded: true,
|
||||
dropdownSearchData: DropdownSearchData(
|
||||
searchController: textEditingController,
|
||||
searchInnerWidgetHeight: 50,
|
||||
searchInnerWidget: Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8,
|
||||
bottom: 4,
|
||||
right: 8,
|
||||
left: 8,
|
||||
),
|
||||
child: TextFormField(
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
controller: textEditingController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
hintText: translate('Search'),
|
||||
hintStyle: const TextStyle(fontSize: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
searchMatchFn: (item, searchValue) {
|
||||
return item.value
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.contains(searchValue.toLowerCase());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTagHeader() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -154,11 +285,12 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return Obx(() {
|
||||
final List tags;
|
||||
if (gFFI.abModel.sortTags.value) {
|
||||
tags = gFFI.abModel.tags.toList();
|
||||
tags = gFFI.abModel.currentAbTags.toList();
|
||||
tags.sort();
|
||||
} else {
|
||||
tags = gFFI.abModel.tags;
|
||||
tags = gFFI.abModel.currentAbTags;
|
||||
}
|
||||
final editPermission = gFFI.abModel.current.canWrite();
|
||||
tagBuilder(String e) {
|
||||
return AddressBookTag(
|
||||
name: e,
|
||||
@@ -169,7 +301,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
} else {
|
||||
gFFI.abModel.selectedTags.add(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
showActionMenu: editPermission);
|
||||
}
|
||||
|
||||
final gridView = DynamicGridView.builder(
|
||||
@@ -181,7 +314,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return tagBuilder(e);
|
||||
});
|
||||
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return isDesktop
|
||||
return (isDesktop || isWebDesktop)
|
||||
? gridView
|
||||
: LimitedBox(maxHeight: maxHeight, child: gridView);
|
||||
});
|
||||
@@ -193,7 +326,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
alignment: Alignment.topLeft,
|
||||
child: AddressBookPeersView(
|
||||
menuPadding: widget.menuPadding,
|
||||
initPeers: gFFI.abModel.peers,
|
||||
getInitPeers: () => gFFI.abModel.currentAbPeers,
|
||||
)),
|
||||
);
|
||||
}
|
||||
@@ -207,7 +340,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return shouldSyncAb();
|
||||
},
|
||||
setter: (bool v) async {
|
||||
bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : '');
|
||||
gFFI.abModel.setShouldAsync(v);
|
||||
},
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
@@ -246,13 +379,23 @@ class _AddressBookState extends State<AddressBook> {
|
||||
}
|
||||
|
||||
void _showMenu(RelativeRect pos) {
|
||||
final canWrite = gFFI.abModel.current.canWrite();
|
||||
final items = [
|
||||
getEntry(translate("Add ID"), abAddId),
|
||||
getEntry(translate("Add Tag"), abAddTag),
|
||||
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
|
||||
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||
sortMenuItem(),
|
||||
syncMenuItem(),
|
||||
if (canWrite) syncMenuItem(),
|
||||
filterMenuItem(),
|
||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||
MenuEntryDivider<String>(),
|
||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||
getEntry(translate("ab_web_console_tip"), () async {
|
||||
final url = await bind.mainGetApiServer();
|
||||
if (await canLaunchUrlString(url)) {
|
||||
launchUrlString(url);
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
mod_menu.showMenu(
|
||||
@@ -271,17 +414,20 @@ class _AddressBookState extends State<AddressBook> {
|
||||
);
|
||||
}
|
||||
|
||||
void abAddId() async {
|
||||
if (gFFI.abModel.isFull(true)) {
|
||||
void addIdToCurrentAb() async {
|
||||
if (gFFI.abModel.isCurrentAbFull(true)) {
|
||||
return;
|
||||
}
|
||||
var isInProgress = false;
|
||||
var passwordVisible = false;
|
||||
IDTextEditingController idController = IDTextEditingController(text: '');
|
||||
TextEditingController aliasController = TextEditingController(text: '');
|
||||
final tags = List.of(gFFI.abModel.tags);
|
||||
TextEditingController passwordController = TextEditingController(text: '');
|
||||
final tags = List.of(gFFI.abModel.currentAbTags);
|
||||
var selectedTag = List<dynamic>.empty(growable: true).obs;
|
||||
final style = TextStyle(fontSize: 14.0);
|
||||
String? errorMsg;
|
||||
final isCurrentAbShared = !gFFI.abModel.current.isPersonal();
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
@@ -293,22 +439,50 @@ class _AddressBookState extends State<AddressBook> {
|
||||
if (id.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
if (gFFI.abModel.idContainBy(id)) {
|
||||
if (gFFI.abModel.idContainByCurrent(id)) {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
errorMsg = translate('ID already exists');
|
||||
});
|
||||
return;
|
||||
}
|
||||
gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag);
|
||||
gFFI.abModel.pushAb();
|
||||
this.setState(() {});
|
||||
var password = '';
|
||||
if (isCurrentAbShared) {
|
||||
password = passwordController.text;
|
||||
}
|
||||
String? errMsg2 = await gFFI.abModel.addIdToCurrent(
|
||||
id, aliasController.text.trim(), password, selectedTag);
|
||||
if (errMsg2 != null) {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
errorMsg = errMsg2;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
double marginBottom = 4;
|
||||
|
||||
row({required Widget lable, required Widget input}) {
|
||||
return Row(
|
||||
children: [
|
||||
!isMobile
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
: SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 200),
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isMobile ? 8 : 0);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Add ID")),
|
||||
content: Column(
|
||||
@@ -316,66 +490,103 @@ class _AddressBookState extends State<AddressBook> {
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'*',
|
||||
style: TextStyle(color: Colors.red, fontSize: 14),
|
||||
),
|
||||
Text(
|
||||
'ID',
|
||||
style: style,
|
||||
),
|
||||
],
|
||||
),
|
||||
).marginOnly(bottom: marginBottom),
|
||||
TextField(
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(errorText: errorMsg),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
row(
|
||||
lable: Row(
|
||||
children: [
|
||||
Text(
|
||||
'*',
|
||||
style: TextStyle(color: Colors.red, fontSize: 14),
|
||||
),
|
||||
Text(
|
||||
'ID',
|
||||
style: style,
|
||||
),
|
||||
],
|
||||
),
|
||||
input: TextField(
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
)),
|
||||
row(
|
||||
lable: Text(
|
||||
translate('Alias'),
|
||||
style: style,
|
||||
),
|
||||
).marginOnly(top: 8, bottom: marginBottom),
|
||||
TextField(
|
||||
controller: aliasController,
|
||||
input: TextField(
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('Alias'),
|
||||
)),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
translate('Tags'),
|
||||
style: style,
|
||||
),
|
||||
).marginOnly(top: 8, bottom: marginBottom),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Wrap(
|
||||
children: tags
|
||||
.map((e) => AddressBookTag(
|
||||
name: e,
|
||||
tags: selectedTag,
|
||||
onTap: () {
|
||||
if (selectedTag.contains(e)) {
|
||||
selectedTag.remove(e);
|
||||
} else {
|
||||
selectedTag.add(e);
|
||||
}
|
||||
if (isCurrentAbShared)
|
||||
row(
|
||||
lable: Text(
|
||||
translate('Password'),
|
||||
style: style,
|
||||
),
|
||||
input: TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
},
|
||||
showActionMenu: false))
|
||||
.toList(growable: false),
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
translate('Tags'),
|
||||
style: style,
|
||||
),
|
||||
).marginOnly(top: 8, bottom: marginBottom),
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Wrap(
|
||||
children: tags
|
||||
.map((e) => AddressBookTag(
|
||||
name: e,
|
||||
tags: selectedTag,
|
||||
onTap: () {
|
||||
if (selectedTag.contains(e)) {
|
||||
selectedTag.remove(e);
|
||||
} else {
|
||||
selectedTag.add(e);
|
||||
}
|
||||
},
|
||||
showActionMenu: false))
|
||||
.toList(growable: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
if (!gFFI.abModel.current.isPersonal())
|
||||
Row(children: [
|
||||
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
|
||||
Text(
|
||||
translate('share_warning_tip'),
|
||||
style: TextStyle(fontSize: 12),
|
||||
)
|
||||
]).marginSymmetric(vertical: 10),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
if (isInProgress) const LinearProgressIndicator(),
|
||||
],
|
||||
@@ -407,10 +618,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
} else {
|
||||
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = tags.join(',');
|
||||
for (final tag in tags) {
|
||||
gFFI.abModel.addTag(tag);
|
||||
}
|
||||
gFFI.abModel.pushAb();
|
||||
gFFI.abModel.addTags(tags);
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
@@ -491,7 +699,7 @@ class AddressBookTag extends StatelessWidget {
|
||||
child: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: tags.contains(name)
|
||||
? gFFI.abModel.getTagColor(name)
|
||||
? gFFI.abModel.getCurrentAbTagColor(name)
|
||||
: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
|
||||
@@ -506,7 +714,7 @@ class AddressBookTag extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
color: tags.contains(name)
|
||||
? Colors.white
|
||||
: gFFI.abModel.getTagColor(name)),
|
||||
: gFFI.abModel.getCurrentAbTagColor(name)),
|
||||
).marginOnly(right: radius / 2),
|
||||
Expanded(
|
||||
child: Text(name,
|
||||
@@ -530,7 +738,8 @@ class AddressBookTag extends StatelessWidget {
|
||||
if (newName == null || newName.isEmpty) {
|
||||
return translate('Can not be empty');
|
||||
}
|
||||
if (newName != name && gFFI.abModel.tags.contains(newName)) {
|
||||
if (newName != name &&
|
||||
gFFI.abModel.currentAbTags.contains(newName)) {
|
||||
return translate('Already exists');
|
||||
}
|
||||
return null;
|
||||
@@ -538,7 +747,6 @@ class AddressBookTag extends StatelessWidget {
|
||||
onSubmit: (String newName) {
|
||||
if (name != newName) {
|
||||
gFFI.abModel.renameTag(name, newName);
|
||||
gFFI.abModel.pushAb();
|
||||
}
|
||||
Future.delayed(Duration.zero, () => Get.back());
|
||||
},
|
||||
@@ -548,7 +756,7 @@ class AddressBookTag extends StatelessWidget {
|
||||
}),
|
||||
getEntry(translate(translate('Change Color')), () async {
|
||||
final model = gFFI.abModel;
|
||||
Color oldColor = model.getTagColor(name);
|
||||
Color oldColor = model.getCurrentAbTagColor(name);
|
||||
Color newColor = await showColorPickerDialog(
|
||||
context,
|
||||
oldColor,
|
||||
@@ -567,12 +775,10 @@ class AddressBookTag extends StatelessWidget {
|
||||
);
|
||||
if (oldColor != newColor) {
|
||||
model.setTagColor(name, newColor);
|
||||
model.pushAb();
|
||||
}
|
||||
}),
|
||||
getEntry(translate("Delete"), () {
|
||||
gFFI.abModel.deleteTag(name);
|
||||
gFFI.abModel.pushAb();
|
||||
Future.delayed(Duration.zero, () => Get.back());
|
||||
}),
|
||||
];
|
||||
|
||||
56
flutter/lib/common/widgets/audio_input.dart
Normal file
56
flutter/lib/common/widgets/audio_input.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
typedef AudioINputSetDevice = void Function(String device);
|
||||
typedef AudioInputBuilder = Widget Function(
|
||||
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
||||
|
||||
class AudioInput extends StatelessWidget {
|
||||
final AudioInputBuilder builder;
|
||||
|
||||
const AudioInput({Key? key, required this.builder}) : super(key: key);
|
||||
|
||||
static String getDefault() {
|
||||
if (isWindows) return translate('System Sound');
|
||||
return '';
|
||||
}
|
||||
|
||||
static Future<String> getValue() async {
|
||||
String device = await bind.mainGetOption(key: 'audio-input');
|
||||
if (device.isNotEmpty) {
|
||||
return device;
|
||||
} else {
|
||||
return getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setDevice(String device) async {
|
||||
if (device == getDefault()) device = '';
|
||||
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||
}
|
||||
|
||||
static Future<Map<String, Object>> getDevicesInfo() async {
|
||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||
if (isWindows) {
|
||||
devices.insert(0, translate('System Sound'));
|
||||
}
|
||||
String current = await getValue();
|
||||
return {'devices': devices, 'current': current};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return futureBuilder(
|
||||
future: getDevicesInfo(),
|
||||
hasData: (data) {
|
||||
String currentDevice = data['current'];
|
||||
List<String> devices = data['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
return const Offstage();
|
||||
}
|
||||
return builder(devices, currentDevice, setDevice);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@ import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||
Future<List<Peer>> getAllPeers() async {
|
||||
Map<String, dynamic> recentPeers = jsonDecode(bind.mainLoadRecentPeersSync());
|
||||
Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
|
||||
Map<String, dynamic> abPeers = jsonDecode(bind.mainLoadAbSync());
|
||||
Map<String, dynamic> groupPeers = jsonDecode(bind.mainLoadGroupSync());
|
||||
|
||||
Map<String, dynamic> combinedPeers = {};
|
||||
|
||||
void mergePeers(Map<String, dynamic> peers) {
|
||||
@@ -42,8 +39,16 @@ Future<List<Peer>> getAllPeers() async {
|
||||
|
||||
mergePeers(recentPeers);
|
||||
mergePeers(lanPeers);
|
||||
mergePeers(abPeers);
|
||||
mergePeers(groupPeers);
|
||||
for (var p in gFFI.abModel.allPeers()) {
|
||||
if (!combinedPeers.containsKey(p.id)) {
|
||||
combinedPeers[p.id] = p.toJson();
|
||||
}
|
||||
}
|
||||
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
|
||||
if (!combinedPeers.containsKey(p.id)) {
|
||||
combinedPeers[p.id] = p.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
List<Peer> parsedPeers = [];
|
||||
|
||||
@@ -181,7 +186,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
|
||||
],
|
||||
))));
|
||||
final colors = _frontN(widget.peer.tags, 25)
|
||||
.map((e) => gFFI.abModel.getTagColor(e))
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
@@ -78,7 +80,7 @@ void changeIdDialog() {
|
||||
final Iterable violations = rules.where((r) => !r.validate(newId));
|
||||
if (violations.isNotEmpty) {
|
||||
setState(() {
|
||||
msg = isDesktop
|
||||
msg = (isDesktop || isWebDesktop)
|
||||
? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'
|
||||
: violations.map((r) => r.name).join(', ');
|
||||
});
|
||||
@@ -103,7 +105,7 @@ void changeIdDialog() {
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
msg = isDesktop
|
||||
msg = (isDesktop || isWebDesktop)
|
||||
? '${translate('Prompt')}: ${translate(status)}'
|
||||
: translate(status);
|
||||
});
|
||||
@@ -140,7 +142,7 @@ void changeIdDialog() {
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
isDesktop
|
||||
(isDesktop || isWebDesktop)
|
||||
? Obx(() => Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 4,
|
||||
@@ -622,7 +624,7 @@ class _DialogVerificationCodeField extends State<DialogVerificationCodeField> {
|
||||
|
||||
// software secure keyboard will take the focus since flutter 3.13
|
||||
// request focus again when android account password obtain focus
|
||||
if (Platform.isAndroid && widget.reRequestFocus) {
|
||||
if (isAndroid && widget.reRequestFocus) {
|
||||
_focusNode.addListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_timerReRequestFocus?.cancel();
|
||||
@@ -691,7 +693,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
}
|
||||
// software secure keyboard will take the focus since flutter 3.13
|
||||
// request focus again when android account password obtain focus
|
||||
if (Platform.isAndroid && widget.reRequestFocus) {
|
||||
if (isAndroid && widget.reRequestFocus) {
|
||||
_focusNode.addListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_timerReRequestFocus?.cancel();
|
||||
@@ -1106,7 +1108,7 @@ void showRequestElevationDialog(
|
||||
errorText: errPwd.isEmpty ? null : errPwd.value,
|
||||
),
|
||||
],
|
||||
).marginOnly(left: isDesktop ? 35 : 0),
|
||||
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
|
||||
).marginOnly(top: 10),
|
||||
],
|
||||
),
|
||||
@@ -1583,7 +1585,7 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||
}
|
||||
|
||||
void deletePeerConfirmDialog(Function onSubmit, String title) async {
|
||||
void deleteConfirmDialog(Function onSubmit, String title) async {
|
||||
gFFI.dialogManager.show(
|
||||
(setState, close, context) {
|
||||
submit() async {
|
||||
@@ -1631,7 +1633,7 @@ void editAbTagDialog(
|
||||
List<dynamic> currentTags, Function(List<dynamic>) onSubmit) {
|
||||
var isInProgress = false;
|
||||
|
||||
final tags = List.of(gFFI.abModel.tags);
|
||||
final tags = List.of(gFFI.abModel.currentAbTags);
|
||||
var selectedTag = currentTags.obs;
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
@@ -1909,3 +1911,207 @@ void showWindowsSessionsDialog(
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void addPeersToAbDialog(
|
||||
List<Peer> peers,
|
||||
) async {
|
||||
Future<bool> addTo(String abname) async {
|
||||
final mapList = peers.map((e) {
|
||||
var json = e.toJson();
|
||||
// remove password when add to another address book to avoid re-share
|
||||
json.remove('password');
|
||||
json.remove('hash');
|
||||
return json;
|
||||
}).toList();
|
||||
final errMsg = await gFFI.abModel.addPeersTo(mapList, abname);
|
||||
if (errMsg == null) {
|
||||
showToast(translate('Successful'));
|
||||
return true;
|
||||
} else {
|
||||
BotToast.showText(text: errMsg, contentColor: Colors.red);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if only one address book and it is personal, add to it directly
|
||||
if (gFFI.abModel.addressbooks.length == 1 &&
|
||||
gFFI.abModel.current.isPersonal()) {
|
||||
await addTo(gFFI.abModel.currentName.value);
|
||||
return;
|
||||
}
|
||||
|
||||
RxBool isInProgress = false.obs;
|
||||
final names = gFFI.abModel.addressBooksCanWrite();
|
||||
RxString currentName = gFFI.abModel.currentName.value.obs;
|
||||
TextEditingController controller = TextEditingController();
|
||||
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
|
||||
names.remove(currentName.value);
|
||||
}
|
||||
if (names.isEmpty) {
|
||||
debugPrint('no address book to add peers to, should not happen');
|
||||
return;
|
||||
}
|
||||
if (!names.contains(currentName.value)) {
|
||||
currentName.value = names[0];
|
||||
}
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
if (controller.text != gFFI.abModel.translatedName(currentName.value)) {
|
||||
BotToast.showText(
|
||||
text: 'illegal address book name: ${controller.text}',
|
||||
contentColor: Colors.red);
|
||||
return;
|
||||
}
|
||||
isInProgress.value = true;
|
||||
if (await addTo(currentName.value)) {
|
||||
close();
|
||||
}
|
||||
isInProgress.value = false;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(IconFont.addressBook, color: MyTheme.accent),
|
||||
Text(translate('Add to address book')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Obx(() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// https://github.com/flutter/flutter/issues/145081
|
||||
DropdownMenu(
|
||||
initialSelection: currentName.value,
|
||||
onSelected: (value) {
|
||||
if (value != null) {
|
||||
currentName.value = value;
|
||||
}
|
||||
},
|
||||
dropdownMenuEntries: names
|
||||
.map((e) => DropdownMenuEntry(
|
||||
value: e, label: gFFI.abModel.translatedName(e)))
|
||||
.toList(),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
isDense: true, border: UnderlineInputBorder()),
|
||||
enableFilter: true,
|
||||
controller: controller,
|
||||
),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
isInProgress.value ? const LinearProgressIndicator() : Offstage()
|
||||
],
|
||||
)),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: cancel,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void setSharedAbPasswordDialog(String abName, Peer peer) {
|
||||
TextEditingController controller = TextEditingController(text: '');
|
||||
RxBool isInProgress = false.obs;
|
||||
RxBool isInputEmpty = true.obs;
|
||||
bool passwordVisible = false;
|
||||
controller.addListener(() {
|
||||
isInputEmpty.value = controller.text.isEmpty;
|
||||
});
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
change(String password) async {
|
||||
isInProgress.value = true;
|
||||
bool res =
|
||||
await gFFI.abModel.changeSharedPassword(abName, peer.id, password);
|
||||
isInProgress.value = false;
|
||||
if (res) {
|
||||
showToast(translate('Successful'));
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.key, color: MyTheme.accent),
|
||||
Text(translate(peer.password.isEmpty
|
||||
? 'Set shared password'
|
||||
: 'Change Password'))
|
||||
.paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Obx(() => Column(children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
passwordVisible = !passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!gFFI.abModel.current.isPersonal())
|
||||
Row(children: [
|
||||
Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
|
||||
Text(
|
||||
translate('share_warning_tip'),
|
||||
style: TextStyle(fontSize: 12),
|
||||
)
|
||||
]).marginSymmetric(vertical: 10),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
isInProgress.value ? const LinearProgressIndicator() : Offstage()
|
||||
])),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: cancel,
|
||||
isOutline: true,
|
||||
),
|
||||
if (peer.password.isNotEmpty)
|
||||
dialogButton(
|
||||
"Remove",
|
||||
icon: Icon(Icons.delete_outline_rounded),
|
||||
onPressed: () => change(''),
|
||||
buttonStyle: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||
),
|
||||
Obx(() => dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed:
|
||||
isInputEmpty.value ? null : () => change(controller.text),
|
||||
)),
|
||||
],
|
||||
onSubmit: isInputEmpty.value ? null : () => change(controller.text),
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,7 +47,10 @@ class _MyGroupState extends State<MyGroup> {
|
||||
err: gFFI.groupModel.groupLoadError,
|
||||
retry: null,
|
||||
close: () => gFFI.groupModel.groupLoadError.value = ''),
|
||||
Expanded(child: isDesktop ? _buildDesktop() : _buildMobile())
|
||||
Expanded(
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? _buildDesktop()
|
||||
: _buildMobile())
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -83,7 +86,7 @@ class _MyGroupState extends State<MyGroup> {
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
initPeers: gFFI.groupModel.peers)),
|
||||
getInitPeers: () => gFFI.groupModel.peers)),
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -115,7 +118,7 @@ class _MyGroupState extends State<MyGroup> {
|
||||
alignment: Alignment.topLeft,
|
||||
child: MyGroupPeerView(
|
||||
menuPadding: widget.menuPadding,
|
||||
initPeers: gFFI.groupModel.peers)),
|
||||
getInitPeers: () => gFFI.groupModel.peers)),
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -164,7 +167,7 @@ class _MyGroupState extends State<MyGroup> {
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return isDesktop
|
||||
return (isDesktop || isWebDesktop)
|
||||
? listView
|
||||
: LimitedBox(maxHeight: maxHeight, child: listView);
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: CustomAppBar(
|
||||
onPanUpdate: onPanUpdate,
|
||||
appBar: isDesktop
|
||||
appBar: (isDesktop || isWebDesktop)
|
||||
? _buildDesktopAppBar(context)
|
||||
: _buildMobileAppBar(context),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
@@ -52,7 +51,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
return _buildDesktop();
|
||||
} else {
|
||||
return _buildMobile();
|
||||
@@ -70,12 +69,12 @@ class _PeerCardState extends State<_PeerCard>
|
||||
peerTabModel.select(peer);
|
||||
} else {
|
||||
if (!isWebDesktop) {
|
||||
connectInPeerTab(context, peer.id, widget.tab);
|
||||
connectInPeerTab(context, peer, widget.tab);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDoubleTap: isWebDesktop
|
||||
? () => connectInPeerTab(context, peer.id, widget.tab)
|
||||
? () => connectInPeerTab(context, peer, widget.tab)
|
||||
: null,
|
||||
onLongPress: () {
|
||||
peerTabModel.select(peer);
|
||||
@@ -140,21 +139,30 @@ class _PeerCardState extends State<_PeerCard>
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||
borderRadius: isMobile
|
||||
? BorderRadius.circular(_tileRadius)
|
||||
: BorderRadius.only(
|
||||
topLeft: Radius.circular(_tileRadius),
|
||||
bottomLeft: Radius.circular(_tileRadius),
|
||||
decoration: BoxDecoration(
|
||||
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||
borderRadius: isMobile
|
||||
? BorderRadius.circular(_tileRadius)
|
||||
: BorderRadius.only(
|
||||
topLeft: Radius.circular(_tileRadius),
|
||||
bottomLeft: Radius.circular(_tileRadius),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
width: isMobile ? 50 : 42,
|
||||
height: isMobile ? 50 : null,
|
||||
child: Stack(
|
||||
children: [
|
||||
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
||||
.paddingAll(6),
|
||||
if (_shouldBuildPasswordIcon(peer))
|
||||
Positioned(
|
||||
top: 1,
|
||||
left: 1,
|
||||
child: Icon(Icons.key, size: 6, color: Colors.white),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
width: isMobile ? 50 : 42,
|
||||
height: isMobile ? 50 : null,
|
||||
child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
||||
.paddingAll(6),
|
||||
),
|
||||
],
|
||||
)),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -199,8 +207,9 @@ class _PeerCardState extends State<_PeerCard>
|
||||
)
|
||||
],
|
||||
);
|
||||
final colors =
|
||||
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
|
||||
final colors = _frontN(peer.tags, 25)
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
? ''
|
||||
@@ -310,14 +319,21 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
);
|
||||
|
||||
final colors =
|
||||
_frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
|
||||
final colors = _frontN(peer.tags, 25)
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: peer.tags.isNotEmpty
|
||||
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
||||
: '',
|
||||
child: Stack(children: [
|
||||
child,
|
||||
if (_shouldBuildPasswordIcon(peer))
|
||||
Positioned(
|
||||
top: 4,
|
||||
left: 12,
|
||||
child: Icon(Icons.key, size: 12, color: Colors.white),
|
||||
),
|
||||
if (colors.isNotEmpty)
|
||||
Positioned(
|
||||
top: 4,
|
||||
@@ -401,6 +417,12 @@ class _PeerCardState extends State<_PeerCard>
|
||||
onPointerUp: (_) => _showPeerMenu(peer.id),
|
||||
child: build_more(context));
|
||||
|
||||
bool _shouldBuildPasswordIcon(Peer peer) {
|
||||
if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) return false;
|
||||
if (gFFI.abModel.current.isPersonal()) return false;
|
||||
return peer.password.isNotEmpty;
|
||||
}
|
||||
|
||||
/// Show the peer menu and handle user's choice.
|
||||
/// User might remove the peer or send a file to the peer.
|
||||
void _showPeerMenu(String id) async {
|
||||
@@ -431,7 +453,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
peer: peer,
|
||||
tab: tab,
|
||||
connect: (BuildContext context, String id) =>
|
||||
connectInPeerTab(context, id, tab),
|
||||
connectInPeerTab(context, peer, tab),
|
||||
popupMenuEntryBuilder: _buildPopupMenuEntry,
|
||||
);
|
||||
}
|
||||
@@ -453,7 +475,6 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
|
||||
MenuEntryBase<String> _connectCommonAction(
|
||||
BuildContext context,
|
||||
String id,
|
||||
String title, {
|
||||
bool isFileTransfer = false,
|
||||
bool isTcpTunneling = false,
|
||||
@@ -467,7 +488,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
proc: () {
|
||||
connectInPeerTab(
|
||||
context,
|
||||
peer.id,
|
||||
peer,
|
||||
tab,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
@@ -480,10 +501,9 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _connectAction(BuildContext context, Peer peer) {
|
||||
MenuEntryBase<String> _connectAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
peer.id,
|
||||
(peer.alias.isEmpty
|
||||
? translate('Connect')
|
||||
: '${translate('Connect')} ${peer.id}'),
|
||||
@@ -491,20 +511,18 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _transferFileAction(BuildContext context, String id) {
|
||||
MenuEntryBase<String> _transferFileAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
id,
|
||||
translate('Transfer file'),
|
||||
isFileTransfer: true,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context, String id) {
|
||||
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
id,
|
||||
translate('TCP tunneling'),
|
||||
isTcpTunneling: true,
|
||||
);
|
||||
@@ -541,7 +559,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
],
|
||||
)),
|
||||
proc: () {
|
||||
connectInPeerTab(context, id, tab, isRDP: true);
|
||||
connectInPeerTab(context, peer, tab, isRDP: true);
|
||||
},
|
||||
padding: menuPadding,
|
||||
dismissOnClicked: true,
|
||||
@@ -648,9 +666,8 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
onSubmit: (String newName) async {
|
||||
if (newName != oldName) {
|
||||
if (tab == PeerTabIndex.ab) {
|
||||
gFFI.abModel.changeAlias(id: id, alias: newName);
|
||||
await gFFI.abModel.changeAlias(id: id, alias: newName);
|
||||
await bind.mainSetPeerAlias(id: id, alias: newName);
|
||||
gFFI.abModel.pushAb();
|
||||
} else {
|
||||
await bind.mainSetPeerAlias(id: id, alias: newName);
|
||||
showToast(translate('Successful'));
|
||||
@@ -702,11 +719,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
await bind.mainLoadLanPeers();
|
||||
break;
|
||||
case PeerTabIndex.ab:
|
||||
gFFI.abModel.deletePeer(id);
|
||||
final future = gFFI.abModel.pushAb();
|
||||
if (await bind.mainPeerExists(id: peer.id)) {
|
||||
gFFI.abModel.reSyncToast(future);
|
||||
}
|
||||
await gFFI.abModel.deletePeers([id]);
|
||||
break;
|
||||
case PeerTabIndex.group:
|
||||
break;
|
||||
@@ -716,7 +729,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
deletePeerConfirmDialog(onSubmit,
|
||||
deleteConfirmDialog(onSubmit,
|
||||
'${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?');
|
||||
},
|
||||
padding: menuPadding,
|
||||
@@ -732,14 +745,16 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
style: style,
|
||||
),
|
||||
proc: () async {
|
||||
bool result = gFFI.abModel.changePassword(id, '');
|
||||
bool succ = await gFFI.abModel.changePersonalHashPassword(id, '');
|
||||
await bind.mainForgetPassword(id: id);
|
||||
bool toast = false;
|
||||
if (result) {
|
||||
toast = tab == PeerTabIndex.ab;
|
||||
gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast);
|
||||
if (succ) {
|
||||
showToast(translate('Successful'));
|
||||
} else {
|
||||
if (tab.index == PeerTabIndex.ab.index) {
|
||||
BotToast.showText(
|
||||
contentColor: Colors.red, text: translate("Failed"));
|
||||
}
|
||||
}
|
||||
if (!toast) showToast(translate('Successful'));
|
||||
},
|
||||
padding: menuPadding,
|
||||
dismissOnClicked: true,
|
||||
@@ -824,13 +839,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
),
|
||||
proc: () {
|
||||
() async {
|
||||
if (gFFI.abModel.isFull(true)) {
|
||||
return;
|
||||
}
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
gFFI.abModel.addPeer(peer);
|
||||
gFFI.abModel.pushAb();
|
||||
}
|
||||
addPeersToAbDialog([Peer.copy(peer)]);
|
||||
}();
|
||||
},
|
||||
padding: menuPadding,
|
||||
@@ -858,25 +867,29 @@ class RecentPeerCard extends BasePeerCard {
|
||||
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context, peer),
|
||||
_transferFileAction(context, peer.id),
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context, peer.id));
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
}
|
||||
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
if (!isWeb) {
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
}
|
||||
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
if (isDesktop || isWebDesktop) {
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
}
|
||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
}
|
||||
@@ -888,9 +901,7 @@ class RecentPeerCard extends BasePeerCard {
|
||||
}
|
||||
|
||||
if (gFFI.userModel.userName.isNotEmpty) {
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
|
||||
menuItems.add(MenuEntryDivider());
|
||||
@@ -915,22 +926,26 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context, peer),
|
||||
_transferFileAction(context, peer.id),
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context, peer.id));
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
}
|
||||
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
if (!isWeb) {
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
}
|
||||
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
if (isDesktop || isWebDesktop) {
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
}
|
||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
}
|
||||
@@ -939,9 +954,7 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
}));
|
||||
|
||||
if (gFFI.userModel.userName.isNotEmpty) {
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
|
||||
menuItems.add(MenuEntryDivider());
|
||||
@@ -966,22 +979,24 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context, peer),
|
||||
_transferFileAction(context, peer.id),
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context, peer.id));
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
}
|
||||
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
if (!isWeb) {
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
}
|
||||
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
menuItems.add(_wolAction(peer.id));
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
|
||||
@@ -992,9 +1007,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
}
|
||||
|
||||
if (gFFI.userModel.userName.isNotEmpty) {
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
|
||||
menuItems.add(MenuEntryDivider());
|
||||
@@ -1019,37 +1032,55 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context, peer),
|
||||
_transferFileAction(context, peer.id),
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context, peer.id));
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
}
|
||||
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
// menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
if (peer.hash.isNotEmpty) {
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
if (gFFI.abModel.current.canWrite()) {
|
||||
menuItems.add(MenuEntryDivider());
|
||||
if (isDesktop || isWebDesktop) {
|
||||
menuItems.add(_renameAction(peer.id));
|
||||
}
|
||||
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
||||
menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
}
|
||||
if (!gFFI.abModel.current.isPersonal()) {
|
||||
menuItems.add(_changeSharedAbPassword());
|
||||
}
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty) {
|
||||
menuItems.add(_editTagAction(peer.id));
|
||||
}
|
||||
}
|
||||
if (gFFI.abModel.tags.isNotEmpty) {
|
||||
menuItems.add(_editTagAction(peer.id));
|
||||
final addressbooks = gFFI.abModel.addressBooksCanWrite();
|
||||
if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
|
||||
addressbooks.remove(gFFI.abModel.currentName.value);
|
||||
}
|
||||
if (addressbooks.isNotEmpty) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
menuItems.add(_existIn());
|
||||
if (gFFI.abModel.current.canWrite()) {
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_removeAction(peer.id));
|
||||
}
|
||||
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(_removeAction(peer.id));
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
// address book does not need to update
|
||||
@protected
|
||||
@override
|
||||
void _update() => gFFI.abModel.pullAb(quiet: true);
|
||||
void _update() =>
|
||||
{}; //gFFI.abModel.pullAb(force: ForcePullAb.current, quiet: true);
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _editTagAction(String id) {
|
||||
@@ -1060,8 +1091,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
),
|
||||
proc: () {
|
||||
editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async {
|
||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||
gFFI.abModel.pushAb();
|
||||
await gFFI.abModel.changeTagForPeers([id], selectedTag);
|
||||
});
|
||||
},
|
||||
padding: super.menuPadding,
|
||||
@@ -1073,6 +1103,53 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
@override
|
||||
Future<String> _getAlias(String id) async =>
|
||||
gFFI.abModel.find(id)?.alias ?? '';
|
||||
|
||||
MenuEntryBase<String> _changeSharedAbPassword() {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate(
|
||||
peer.password.isEmpty ? 'Set shared password' : 'Change Password'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
setSharedAbPasswordDialog(gFFI.abModel.currentName.value, peer);
|
||||
},
|
||||
padding: super.menuPadding,
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
}
|
||||
|
||||
MenuEntryBase<String> _existIn() {
|
||||
final names = gFFI.abModel.idExistIn(peer.id);
|
||||
final text = names.join(', ');
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Exist in'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Exist in')),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [Text(text)]),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: close,
|
||||
),
|
||||
],
|
||||
onSubmit: close,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
},
|
||||
padding: super.menuPadding,
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyGroupPeerCard extends BasePeerCard {
|
||||
@@ -1087,18 +1164,18 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
Future<List<MenuEntryBase<String>>> _buildMenuItems(
|
||||
BuildContext context) async {
|
||||
final List<MenuEntryBase<String>> menuItems = [
|
||||
_connectAction(context, peer),
|
||||
_transferFileAction(context, peer.id),
|
||||
_connectAction(context),
|
||||
if (!isWeb) _transferFileAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context, peer.id));
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
}
|
||||
// menuItems.add(await _openNewConnInOptAction(peer.id));
|
||||
// menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
if (Platform.isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
if (isWindows && peer.platform == kPeerPlatformWindows) {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
menuItems.add(_createShortCutAction(peer.id));
|
||||
}
|
||||
// menuItems.add(MenuEntryDivider());
|
||||
@@ -1107,9 +1184,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
// menuItems.add(_unrememberPasswordAction(peer.id));
|
||||
// }
|
||||
if (gFFI.userModel.userName.isNotEmpty) {
|
||||
if (!gFFI.abModel.idContainBy(peer.id)) {
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
menuItems.add(_addToAb(peer));
|
||||
}
|
||||
return menuItems;
|
||||
}
|
||||
@@ -1176,7 +1251,7 @@ void _rdpDialog(String id) async {
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
Row(
|
||||
children: [
|
||||
isDesktop
|
||||
(isDesktop || isWebDesktop)
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
@@ -1187,15 +1262,17 @@ void _rdpDialog(String id) async {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: isDesktop ? null : translate('Username')),
|
||||
labelText: (isDesktop || isWebDesktop)
|
||||
? null
|
||||
: translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
|
||||
Row(
|
||||
children: [
|
||||
isDesktop
|
||||
(isDesktop || isWebDesktop)
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
@@ -1207,7 +1284,9 @@ void _rdpDialog(String id) async {
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: isDesktop ? null : translate('Password'),
|
||||
labelText: (isDesktop || isWebDesktop)
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
@@ -1305,24 +1384,32 @@ class TagPainter extends CustomPainter {
|
||||
}
|
||||
}
|
||||
|
||||
void connectInPeerTab(BuildContext context, String id, PeerTabIndex tab,
|
||||
void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
{bool isFileTransfer = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false}) async {
|
||||
var password = '';
|
||||
bool isSharedPassword = false;
|
||||
if (tab == PeerTabIndex.ab) {
|
||||
// If recent peer's alias is empty, set it to ab's alias
|
||||
// Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful
|
||||
Peer? p = gFFI.abModel.find(id);
|
||||
if (p != null &&
|
||||
p.alias.isNotEmpty &&
|
||||
(await bind.mainGetPeerOption(id: id, key: "alias")).isEmpty) {
|
||||
if (peer.alias.isNotEmpty &&
|
||||
(await bind.mainGetPeerOption(id: peer.id, key: "alias")).isEmpty) {
|
||||
await bind.mainSetPeerAlias(
|
||||
id: id,
|
||||
alias: p.alias,
|
||||
id: peer.id,
|
||||
alias: peer.alias,
|
||||
);
|
||||
}
|
||||
if (!gFFI.abModel.current.isPersonal()) {
|
||||
if (peer.password.isNotEmpty) {
|
||||
password = peer.password;
|
||||
isSharedPassword = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
connect(context, id,
|
||||
connect(context, peer.id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP);
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
|
||||
as mod_menu;
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -36,7 +37,7 @@ class _TabEntry {
|
||||
}
|
||||
|
||||
EdgeInsets? _menuPadding() {
|
||||
return isDesktop ? kDesktopMenuPadding : null;
|
||||
return (isDesktop || isWebDesktop) ? kDesktopMenuPadding : null;
|
||||
}
|
||||
|
||||
class _PeerTabPageState extends State<PeerTabPage>
|
||||
@@ -61,7 +62,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
AddressBook(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
({dynamic hint}) => gFFI.abModel.pullAb(force: hint == null)),
|
||||
({dynamic hint}) => gFFI.abModel.pullAb(
|
||||
force: hint == null ? ForcePullAb.listAndCurrent : null,
|
||||
quiet: false)),
|
||||
_TabEntry(
|
||||
MyGroup(
|
||||
menuPadding: _menuPadding(),
|
||||
@@ -110,7 +113,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Container(
|
||||
padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: (isDesktop || isWebDesktop)
|
||||
? null
|
||||
: EdgeInsets.symmetric(horizontal: 2),
|
||||
child: selectionWrap(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -124,7 +129,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
],
|
||||
)),
|
||||
),
|
||||
).paddingOnly(right: isDesktop ? 12 : 0),
|
||||
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
|
||||
_createPeersView(),
|
||||
],
|
||||
);
|
||||
@@ -132,11 +137,13 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
|
||||
Widget _createSwitchBar(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
|
||||
return ListView(
|
||||
var counter = -1;
|
||||
return ReorderableListView(
|
||||
buildDefaultDragHandles: false,
|
||||
onReorder: model.reorder,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: model.visibleIndexs.map((t) {
|
||||
children: model.visibleEnabledOrderedIndexs.map((t) {
|
||||
final selected = model.currentTab == t;
|
||||
final color = selected
|
||||
? MyTheme.tabbar(context).selectedTextColor
|
||||
@@ -150,48 +157,54 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
border: Border(
|
||||
bottom: BorderSide(width: 2, color: color!),
|
||||
));
|
||||
return Obx(() => InkWell(
|
||||
child: Container(
|
||||
decoration: (hover.value
|
||||
? (selected ? decoBorder : deco)
|
||||
: (selected ? decoBorder : null)),
|
||||
child: Tooltip(
|
||||
counter += 1;
|
||||
return ReorderableDragStartListener(
|
||||
key: ValueKey(t),
|
||||
index: counter,
|
||||
child: Obx(() => Tooltip(
|
||||
preferBelow: false,
|
||||
message: model.tabTooltip(t),
|
||||
onTriggered: isMobile ? mobileShowTabVisibilityMenu : null,
|
||||
child: Icon(model.tabIcon(t), color: color),
|
||||
).paddingSymmetric(horizontal: 4),
|
||||
).paddingSymmetric(horizontal: 4),
|
||||
onTap: () async {
|
||||
await handleTabSelection(t);
|
||||
await bind.setLocalFlutterOption(
|
||||
k: 'peer-tab-index', v: t.toString());
|
||||
},
|
||||
onHover: (value) => hover.value = value,
|
||||
));
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
decoration: (hover.value
|
||||
? (selected ? decoBorder : deco)
|
||||
: (selected ? decoBorder : null)),
|
||||
child: Icon(model.tabIcon(t), color: color)
|
||||
.paddingSymmetric(horizontal: 4),
|
||||
).paddingSymmetric(horizontal: 4),
|
||||
onTap: () async {
|
||||
await handleTabSelection(t);
|
||||
await bind.setLocalFlutterOption(
|
||||
k: PeerTabModel.kPeerTabIndex, v: t.toString());
|
||||
},
|
||||
onHover: (value) => hover.value = value,
|
||||
),
|
||||
)));
|
||||
}).toList());
|
||||
}
|
||||
|
||||
Widget _createPeersView() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget child;
|
||||
if (model.visibleIndexs.isEmpty) {
|
||||
if (model.visibleEnabledOrderedIndexs.isEmpty) {
|
||||
child = visibleContextMenuListener(Row(
|
||||
children: [Expanded(child: InkWell())],
|
||||
));
|
||||
} else {
|
||||
if (model.visibleIndexs.contains(model.currentTab)) {
|
||||
if (model.visibleEnabledOrderedIndexs.contains(model.currentTab)) {
|
||||
child = entries[model.currentTab].widget;
|
||||
} else {
|
||||
debugPrint("should not happen! currentTab not in visibleIndexs");
|
||||
Future.delayed(Duration.zero, () {
|
||||
model.setCurrentTab(model.indexs[0]);
|
||||
model.setCurrentTab(model.visibleEnabledOrderedIndexs[0]);
|
||||
});
|
||||
child = entries[0].widget;
|
||||
}
|
||||
}
|
||||
return Expanded(
|
||||
child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
|
||||
child: child.marginSymmetric(
|
||||
vertical: (isDesktop || isWebDesktop) ? 12.0 : 6.0));
|
||||
}
|
||||
|
||||
Widget _createRefresh(
|
||||
@@ -200,22 +213,23 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
return Offstage(
|
||||
offstage: model.currentTab != index.index,
|
||||
child: RefreshWidget(
|
||||
onPressed: () {
|
||||
if (gFFI.peerTabModel.currentTab < entries.length) {
|
||||
entries[gFFI.peerTabModel.currentTab].load();
|
||||
}
|
||||
},
|
||||
spinning: loading,
|
||||
child: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Tooltip(
|
||||
message: translate('Refresh'),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
size: 18,
|
||||
color: textColor,
|
||||
)))),
|
||||
child: Tooltip(
|
||||
message: translate('Refresh'),
|
||||
child: RefreshWidget(
|
||||
onPressed: () {
|
||||
if (gFFI.peerTabModel.currentTab < entries.length) {
|
||||
entries[gFFI.peerTabModel.currentTab].load();
|
||||
}
|
||||
},
|
||||
spinning: loading,
|
||||
child: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
size: 18,
|
||||
color: textColor,
|
||||
))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,6 +241,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
return _hoverAction(
|
||||
toolTip: translate('Select'),
|
||||
context: context,
|
||||
onTap: () {
|
||||
model.setMultiSelectionMode(true);
|
||||
@@ -234,30 +249,29 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Select'),
|
||||
child: SvgPicture.asset(
|
||||
"assets/checkbox-outline.svg",
|
||||
width: 18,
|
||||
height: 18,
|
||||
colorFilter: svgColor(textColor),
|
||||
)),
|
||||
child: SvgPicture.asset(
|
||||
"assets/checkbox-outline.svg",
|
||||
width: 18,
|
||||
height: 18,
|
||||
colorFilter: svgColor(textColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void mobileShowTabVisibilityMenu() {
|
||||
final model = gFFI.peerTabModel;
|
||||
final items = List<PopupMenuItem>.empty(growable: true);
|
||||
for (int i = 0; i < model.tabNames.length; i++) {
|
||||
for (int i = 0; i < PeerTabModel.maxTabCount; i++) {
|
||||
if (!model.isEnabled[i]) continue;
|
||||
items.add(PopupMenuItem(
|
||||
height: kMinInteractiveDimension * 0.8,
|
||||
onTap: () => model.setTabVisible(i, !model.isVisible[i]),
|
||||
onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: model.isVisible[i],
|
||||
value: model.isVisibleEnabled[i],
|
||||
onChanged: (_) {
|
||||
model.setTabVisible(i, !model.isVisible[i]);
|
||||
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
@@ -307,16 +321,17 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
|
||||
Widget visibleContextMenu(CancelFunc cancelFunc) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
final menu = List<MenuEntrySwitch>.empty(growable: true);
|
||||
for (int i = 0; i < model.tabNames.length; i++) {
|
||||
menu.add(MenuEntrySwitch(
|
||||
final menu = List<MenuEntrySwitchSync>.empty(growable: true);
|
||||
for (int i = 0; i < model.orders.length; i++) {
|
||||
int tabIndex = model.orders[i];
|
||||
if (tabIndex < 0 || tabIndex >= PeerTabModel.maxTabCount) continue;
|
||||
if (!model.isEnabled[tabIndex]) continue;
|
||||
menu.add(MenuEntrySwitchSync(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: model.tabTooltip(i),
|
||||
getter: () async {
|
||||
return model.isVisible[i];
|
||||
},
|
||||
text: model.tabTooltip(tabIndex),
|
||||
currentValue: model.isVisibleEnabled[tabIndex],
|
||||
setter: (show) async {
|
||||
model.setTabVisible(i, show);
|
||||
model.setTabVisible(tabIndex, show);
|
||||
cancelFunc();
|
||||
}));
|
||||
}
|
||||
@@ -367,6 +382,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
}
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Delete'),
|
||||
onTap: () {
|
||||
onSubmit() async {
|
||||
final peers = model.selectedPeers;
|
||||
@@ -392,21 +408,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
await bind.mainLoadLanPeers();
|
||||
break;
|
||||
case 3:
|
||||
{
|
||||
bool hasSynced = false;
|
||||
if (shouldSyncAb()) {
|
||||
for (var p in peers) {
|
||||
if (await bind.mainPeerExists(id: p.id)) {
|
||||
hasSynced = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
|
||||
final future = gFFI.abModel.pushAb();
|
||||
if (hasSynced) {
|
||||
gFFI.abModel.reSyncToast(future);
|
||||
}
|
||||
}
|
||||
await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -415,11 +417,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
if (model.currentTab != 3) showToast(translate('Successful'));
|
||||
}
|
||||
|
||||
deletePeerConfirmDialog(onSubmit, translate('Delete'));
|
||||
deleteConfirmDialog(onSubmit, translate('Delete'));
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Delete'),
|
||||
child: Icon(Icons.delete, color: Colors.red)));
|
||||
child: Icon(Icons.delete, color: Colors.red));
|
||||
}
|
||||
|
||||
Widget addSelectionToFav() {
|
||||
@@ -429,6 +429,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
model.currentTab != PeerTabIndex.recent.index, // show based on recent
|
||||
child: _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Add to Favorites'),
|
||||
onTap: () async {
|
||||
final peers = model.selectedPeers;
|
||||
final favs = (await bind.mainGetFav()).toList();
|
||||
@@ -441,37 +442,28 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
model.setMultiSelectionMode(false);
|
||||
showToast(translate('Successful'));
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Add to Favorites'),
|
||||
child: Icon(model.icons[PeerTabIndex.fav.index])),
|
||||
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
Widget addSelectionToAb() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
final addressbooks = gFFI.abModel.addressBooksCanWrite();
|
||||
if (model.currentTab == PeerTabIndex.ab.index) {
|
||||
addressbooks.remove(gFFI.abModel.currentName.value);
|
||||
}
|
||||
return Offstage(
|
||||
offstage:
|
||||
!gFFI.userModel.isLogin || model.currentTab == PeerTabIndex.ab.index,
|
||||
offstage: !gFFI.userModel.isLogin || addressbooks.isEmpty,
|
||||
child: _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Add to address book'),
|
||||
onTap: () {
|
||||
if (gFFI.abModel.isFull(true)) {
|
||||
return;
|
||||
}
|
||||
final peers = model.selectedPeers;
|
||||
gFFI.abModel.addPeers(peers);
|
||||
final future = gFFI.abModel.pushAb();
|
||||
final peers = model.selectedPeers.map((e) => Peer.copy(e)).toList();
|
||||
addPeersToAbDialog(peers);
|
||||
model.setMultiSelectionMode(false);
|
||||
Future.delayed(Duration.zero, () async {
|
||||
await future;
|
||||
await Future.delayed(Duration(seconds: 2)); // toast
|
||||
gFFI.abModel.isFull(true);
|
||||
});
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Add to address book'),
|
||||
child: Icon(model.icons[PeerTabIndex.ab.index])),
|
||||
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
);
|
||||
}
|
||||
@@ -481,21 +473,20 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
return Offstage(
|
||||
offstage: !gFFI.userModel.isLogin ||
|
||||
model.currentTab != PeerTabIndex.ab.index ||
|
||||
gFFI.abModel.tags.isEmpty,
|
||||
gFFI.abModel.currentAbTags.isEmpty,
|
||||
child: _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Edit Tag'),
|
||||
onTap: () {
|
||||
editAbTagDialog(List.empty(), (selectedTags) async {
|
||||
final peers = model.selectedPeers;
|
||||
gFFI.abModel.changeTagForPeers(
|
||||
await gFFI.abModel.changeTagForPeers(
|
||||
peers.map((p) => p.id).toList(), selectedTags);
|
||||
gFFI.abModel.pushAb();
|
||||
model.setMultiSelectionMode(false);
|
||||
showToast(translate('Successful'));
|
||||
});
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Edit Tag'), child: Icon(Icons.tag)))
|
||||
child: Icon(Icons.tag))
|
||||
.marginOnly(left: isMobile ? 11 : 6),
|
||||
);
|
||||
}
|
||||
@@ -514,11 +505,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
model.selectedPeers.length >= model.currentTabCachedPeers.length,
|
||||
child: _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Select All'),
|
||||
onTap: () {
|
||||
model.selectAll();
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Select All'), child: Icon(Icons.select_all)),
|
||||
child: Icon(Icons.select_all),
|
||||
).marginOnly(left: 6),
|
||||
);
|
||||
}
|
||||
@@ -527,24 +518,23 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Close'),
|
||||
onTap: () {
|
||||
model.setMultiSelectionMode(false);
|
||||
},
|
||||
child:
|
||||
Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
|
||||
child: Icon(Icons.clear))
|
||||
.marginOnly(left: 6);
|
||||
}
|
||||
|
||||
Widget _toggleTags() {
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Toggle tags'),
|
||||
hoverableWhenfalse: hideAbTagsPanel,
|
||||
child: Tooltip(
|
||||
message: translate('Toggle Tags'),
|
||||
child: Icon(
|
||||
Icons.tag_rounded,
|
||||
size: 18,
|
||||
)),
|
||||
child: Icon(
|
||||
Icons.tag_rounded,
|
||||
size: 18,
|
||||
),
|
||||
onTap: () async {
|
||||
await bind.mainSetLocalOption(
|
||||
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
|
||||
@@ -556,7 +546,8 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
return [
|
||||
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
|
||||
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
|
||||
_createRefresh(
|
||||
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
||||
_createRefresh(
|
||||
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
|
||||
Offstage(
|
||||
@@ -580,7 +571,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
|
||||
final leftActionsSize =
|
||||
(leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length;
|
||||
(leftIconSize + (4 + 4) * 2) * model.visibleEnabledOrderedIndexs.length;
|
||||
final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2;
|
||||
final searchWidth = 120;
|
||||
final otherActionWidth = 18 + 10;
|
||||
@@ -593,14 +584,13 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
(BuildContext context, Future<void> Function() showMenu) {
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
child: Tooltip(
|
||||
message: translate('More'),
|
||||
child: SvgPicture.asset(
|
||||
"assets/chevron_up_chevron_down.svg",
|
||||
width: 18,
|
||||
height: 18,
|
||||
colorFilter: svgColor(textColor),
|
||||
)),
|
||||
toolTip: translate('More'),
|
||||
child: SvgPicture.asset(
|
||||
"assets/chevron_up_chevron_down.svg",
|
||||
width: 18,
|
||||
height: 18,
|
||||
colorFilter: svgColor(textColor),
|
||||
),
|
||||
onTap: showMenu,
|
||||
);
|
||||
},
|
||||
@@ -624,7 +614,8 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
List<Widget> actions = [
|
||||
const PeerSearchBar(),
|
||||
if (model.currentTab == PeerTabIndex.ab.index)
|
||||
_createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading),
|
||||
_createRefresh(
|
||||
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
||||
if (model.currentTab == PeerTabIndex.group.index)
|
||||
_createRefresh(
|
||||
index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading),
|
||||
@@ -674,18 +665,17 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
? _buildSearchBar()
|
||||
: _hoverAction(
|
||||
context: context,
|
||||
toolTip: translate('Search'),
|
||||
padding: const EdgeInsets.only(right: 2),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
drawer = true;
|
||||
});
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Search'),
|
||||
child: Icon(
|
||||
Icons.search_rounded,
|
||||
color: Theme.of(context).hintColor,
|
||||
)));
|
||||
child: Icon(
|
||||
Icons.search_rounded,
|
||||
color: Theme.of(context).hintColor,
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
@@ -826,16 +816,15 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
child: Tooltip(
|
||||
message: translate('Change view'),
|
||||
child: Icon(
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? Icons.grid_view_rounded
|
||||
: peerCardUiType.value == PeerUiType.tile
|
||||
? Icons.view_list_rounded
|
||||
: Icons.view_agenda_rounded,
|
||||
size: 18,
|
||||
)),
|
||||
toolTip: translate('Change view'),
|
||||
child: Icon(
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? Icons.grid_view_rounded
|
||||
: peerCardUiType.value == PeerUiType.tile
|
||||
? Icons.view_list_rounded
|
||||
: Icons.view_agenda_rounded,
|
||||
size: 18,
|
||||
),
|
||||
onTapDown: (details) {
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
@@ -905,12 +894,11 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
return _hoverAction(
|
||||
context: context,
|
||||
child: Tooltip(
|
||||
message: translate('Sort by'),
|
||||
child: Icon(
|
||||
Icons.sort_rounded,
|
||||
size: 18,
|
||||
)),
|
||||
toolTip: translate('Sort by'),
|
||||
child: Icon(
|
||||
Icons.sort_rounded,
|
||||
size: 18,
|
||||
),
|
||||
onTapDown: (details) {
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
@@ -992,6 +980,7 @@ Widget _hoverAction(
|
||||
{required BuildContext context,
|
||||
required Widget child,
|
||||
required Function() onTap,
|
||||
required String toolTip,
|
||||
GestureTapDownCallback? onTapDown,
|
||||
RxBool? hoverableWhenfalse,
|
||||
EdgeInsetsGeometry padding = const EdgeInsets.all(4.0)}) {
|
||||
@@ -1000,16 +989,19 @@ Widget _hoverAction(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
);
|
||||
return Obx(
|
||||
() => Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration:
|
||||
(hover.value || hoverableWhenfalse?.value == false) ? deco : null,
|
||||
child: InkWell(
|
||||
onHover: (value) => hover.value = value,
|
||||
onTap: onTap,
|
||||
onTapDown: onTapDown,
|
||||
child: Container(padding: padding, child: child))),
|
||||
return Tooltip(
|
||||
message: toolTip,
|
||||
child: Obx(
|
||||
() => Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration:
|
||||
(hover.value || hoverableWhenfalse?.value == false) ? deco : null,
|
||||
child: InkWell(
|
||||
onHover: (value) => hover.value = value,
|
||||
onTap: onTap,
|
||||
onTapDown: onTapDown,
|
||||
child: Container(padding: padding, child: child))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
LoadEvent.lan: 'empty_lan_tip',
|
||||
LoadEvent.addressBook: 'empty_address_book_tip',
|
||||
});
|
||||
final space = isDesktop ? 12.0 : 8.0;
|
||||
final space = (isDesktop || isWebDesktop) ? 12.0 : 8.0;
|
||||
final _curPeers = <String>{};
|
||||
var _lastChangeTime = DateTime.now();
|
||||
var _lastQueryPeers = <String>{};
|
||||
@@ -86,17 +86,6 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
var _queryCount = 0;
|
||||
var _exit = false;
|
||||
|
||||
late final mobileWidth = () {
|
||||
const minWidth = 320.0;
|
||||
final windowWidth = MediaQuery.of(context).size.width;
|
||||
var width = windowWidth - 2 * space;
|
||||
if (windowWidth > minWidth + 2 * space) {
|
||||
final n = (windowWidth / (minWidth + 2 * space)).floor();
|
||||
width = windowWidth / n - 2 * space;
|
||||
}
|
||||
return width;
|
||||
}();
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
_PeersViewState() {
|
||||
@@ -189,54 +178,60 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
onVisibilityChanged: onVisibilityChanged,
|
||||
child: widget.peerCardBuilder(peer),
|
||||
);
|
||||
final windowWidth = MediaQuery.of(context).size.width;
|
||||
// `Provider.of<PeerTabModel>(context)` will causes infinete loop.
|
||||
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
|
||||
//
|
||||
// No need to listen the currentTab change event.
|
||||
// Because the currentTab change event will trigger the peers change event,
|
||||
// and the peers change event will trigger _buildPeersView().
|
||||
final currentTab = Provider.of<PeerTabModel>(context, listen: false).currentTab;
|
||||
final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
||||
return isDesktop
|
||||
? Obx(
|
||||
() => SizedBox(
|
||||
width: peerCardUiType.value != PeerUiType.list
|
||||
? 220
|
||||
: currentTab == PeerTabIndex.group.index || (currentTab == PeerTabIndex.ab.index && !hideAbTagsPanel)
|
||||
? windowWidth - 390 :
|
||||
windowWidth - 227,
|
||||
height:
|
||||
peerCardUiType.value == PeerUiType.grid ? 140 : peerCardUiType.value != PeerUiType.list ? 42 : 45,
|
||||
child: visibilityChild,
|
||||
),
|
||||
)
|
||||
: SizedBox(width: mobileWidth, child: visibilityChild);
|
||||
return (isDesktop || isWebDesktop)
|
||||
? Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? Container(height: 45, child: visibilityChild)
|
||||
: peerCardUiType.value == PeerUiType.grid
|
||||
? SizedBox(
|
||||
width: 220, height: 140, child: visibilityChild)
|
||||
: SizedBox(
|
||||
width: 220, height: 42, child: visibilityChild))
|
||||
: Container(child: visibilityChild);
|
||||
}
|
||||
|
||||
final Widget child;
|
||||
if (isMobile) {
|
||||
child = DynamicGridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2, crossAxisSpacing: space),
|
||||
child = ListView.builder(
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
return buildOnePeer(peers[index]).marginOnly(
|
||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child = DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2, crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
}),
|
||||
);
|
||||
child = Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]).marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
}),
|
||||
)
|
||||
: DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
if (updateEvent == UpdateEvent.load) {
|
||||
@@ -354,7 +349,7 @@ abstract class BasePeersView extends StatelessWidget {
|
||||
final String loadEvent;
|
||||
final PeerFilter? peerFilter;
|
||||
final PeerCardBuilder peerCardBuilder;
|
||||
final RxList<Peer>? initPeers;
|
||||
final GetInitPeers? getInitPeers;
|
||||
|
||||
const BasePeersView({
|
||||
Key? key,
|
||||
@@ -362,13 +357,14 @@ abstract class BasePeersView extends StatelessWidget {
|
||||
required this.loadEvent,
|
||||
this.peerFilter,
|
||||
required this.peerCardBuilder,
|
||||
required this.initPeers,
|
||||
required this.getInitPeers,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _PeersView(
|
||||
peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers),
|
||||
peers:
|
||||
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
|
||||
peerFilter: peerFilter,
|
||||
peerCardBuilder: peerCardBuilder);
|
||||
}
|
||||
@@ -385,7 +381,7 @@ class RecentPeersView extends BasePeersView {
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
initPeers: null,
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -407,7 +403,7 @@ class FavoritePeersView extends BasePeersView {
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
initPeers: null,
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -429,7 +425,7 @@ class DiscoveredPeersView extends BasePeersView {
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
initPeers: null,
|
||||
getInitPeers: null,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -445,7 +441,7 @@ class AddressBookPeersView extends BasePeersView {
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required RxList<Peer> initPeers})
|
||||
required GetInitPeers getInitPeers})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'address book peer',
|
||||
@@ -456,7 +452,7 @@ class AddressBookPeersView extends BasePeersView {
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
initPeers: initPeers,
|
||||
getInitPeers: getInitPeers,
|
||||
);
|
||||
|
||||
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||
@@ -486,7 +482,7 @@ class MyGroupPeerView extends BasePeersView {
|
||||
{Key? key,
|
||||
EdgeInsets? menuPadding,
|
||||
ScrollController? scrollController,
|
||||
required RxList<Peer> initPeers})
|
||||
required GetInitPeers getInitPeers})
|
||||
: super(
|
||||
key: key,
|
||||
name: 'group peer',
|
||||
@@ -496,7 +492,7 @@ class MyGroupPeerView extends BasePeersView {
|
||||
peer: peer,
|
||||
menuPadding: menuPadding,
|
||||
),
|
||||
initPeers: initPeers,
|
||||
getInitPeers: getInitPeers,
|
||||
);
|
||||
|
||||
static bool filter(Peer peer) {
|
||||
|
||||
@@ -77,7 +77,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
InputModel get inputModel => widget.inputModel;
|
||||
bool get handleTouch => isDesktop || ffiModel.touchMode;
|
||||
bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode;
|
||||
SessionID get sessionId => ffi.sessionId;
|
||||
|
||||
@override
|
||||
@@ -183,7 +183,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop || !ffiModel.touchMode) {
|
||||
if ((isDesktop || isWebDesktop) || !ffiModel.touchMode) {
|
||||
inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +222,9 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
if (isDesktop) {
|
||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||
}
|
||||
inputModel.sendMouse('down', MouseButtons.left);
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
} else {
|
||||
@@ -241,13 +244,16 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
|
||||
onOneFingerPanEnd(DragEndDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
ffi.cursorModel.clearRemoteWindowCoords();
|
||||
}
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
|
||||
@@ -262,7 +268,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
final scale = ((d.scale - _scale) * 1000).toInt();
|
||||
_scale = d.scale;
|
||||
|
||||
@@ -286,7 +292,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
if ((isDesktop || isWebDesktop)) {
|
||||
bind.sessionSendPointer(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(
|
||||
@@ -409,7 +415,9 @@ class RawPointerMouseRegion extends StatelessWidget {
|
||||
onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
|
||||
onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
|
||||
child: MouseRegion(
|
||||
cursor: cursor ?? MouseCursor.defer,
|
||||
cursor: inputModel.isViewOnly
|
||||
? MouseCursor.defer
|
||||
: (cursor ?? MouseCursor.defer),
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
child: child,
|
||||
|
||||
@@ -209,10 +209,12 @@ List<Widget> ServerConfigImportExportWidgets(
|
||||
List<(String, String)> otherDefaultSettings() {
|
||||
List<(String, String)> v = [
|
||||
('View Mode', 'view_only'),
|
||||
if (isDesktop) ('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||
if (isDesktop) ('Collapse toolbar', 'collapse_toolbar'),
|
||||
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
|
||||
('Show remote cursor', 'show_remote_cursor'),
|
||||
if (isDesktop) ('Zoom cursor', 'zoom-cursor'),
|
||||
('Follow remote cursor', 'follow_remote_cursor'),
|
||||
('Follow remote window focus', 'follow_remote_window'),
|
||||
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
|
||||
('Show quality monitor', 'show_quality_monitor'),
|
||||
('Mute', 'disable_audio'),
|
||||
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),
|
||||
@@ -221,7 +223,7 @@ List<(String, String)> otherDefaultSettings() {
|
||||
('Privacy mode', 'privacy_mode'),
|
||||
if (isMobile) ('Touch mode', 'touch-mode'),
|
||||
('True color (4:4:4)', 'i444'),
|
||||
('Reverse mouse wheel', 'reverse_mouse_wheel'),
|
||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||
('swap-left-right-mouse', 'swap-left-right-mouse'),
|
||||
if (isDesktop && useTextureRender)
|
||||
(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -95,7 +94,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')),
|
||||
]),
|
||||
trailingIcon: Transform.scale(
|
||||
scale: isDesktop ? 0.8 : 1,
|
||||
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (isMobile && Navigator.canPop(context)) {
|
||||
@@ -161,7 +160,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// divider
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
|
||||
}
|
||||
// ctrlAltDel
|
||||
@@ -230,7 +229,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
));
|
||||
}
|
||||
// record
|
||||
if (!isDesktop &&
|
||||
if (!(isDesktop || isWeb) &&
|
||||
(ffi.recordingModel.start || (perms["recording"] != false))) {
|
||||
v.add(TTextMenu(
|
||||
child: Row(
|
||||
@@ -251,7 +250,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
onPressed: () => ffi.recordingModel.toggle()));
|
||||
}
|
||||
// fingerprint
|
||||
if (!isDesktop) {
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Copy Fingerprint')),
|
||||
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
|
||||
@@ -356,14 +355,18 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
|
||||
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
||||
return TRadioMenu<String>(
|
||||
child: Text(translate(label)),
|
||||
child: Text(label),
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: enabled ? onChanged : null);
|
||||
}
|
||||
|
||||
var autoLabel = translate('Auto');
|
||||
if (groupValue == 'auto') {
|
||||
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
||||
}
|
||||
return [
|
||||
radio('Auto', 'auto', true),
|
||||
radio(autoLabel, 'auto', true),
|
||||
if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
||||
radio('VP9', 'vp9', true),
|
||||
if (codecs[1]) radio('AV1', 'av1', codecs[1]),
|
||||
@@ -372,7 +375,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
Future<List<TToggleMenu>> toolbarCursor(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
List<TToggleMenu> v = [];
|
||||
final ffiModel = ffi.ffiModel;
|
||||
@@ -385,12 +388,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland) {
|
||||
final state = ShowRemoteCursorState.find(id);
|
||||
final lockState = ShowRemoteCursorLockState.find(id);
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'show-remote-cursor';
|
||||
if (pi.currentDisplay == kAllDisplayValue ||
|
||||
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
lockState.value = false;
|
||||
}
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Show remote cursor')),
|
||||
value: state.value,
|
||||
onChanged: enabled
|
||||
onChanged: enabled && !lockState.value
|
||||
? (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
@@ -400,6 +408,67 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
}
|
||||
: null));
|
||||
}
|
||||
// follow remote cursor
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland &&
|
||||
versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||
pi.displays.length > 1 &&
|
||||
pi.currentDisplay != kAllDisplayValue &&
|
||||
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
final option = 'follow-remote-cursor';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
final showCursorOption = 'show-remote-cursor';
|
||||
final showCursorState = ShowRemoteCursorState.find(id);
|
||||
final showCursorLockState = ShowRemoteCursorLockState.find(id);
|
||||
final showCursorEnabled = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
showCursorLockState.value = value;
|
||||
if (value && !showCursorEnabled) {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: showCursorOption);
|
||||
showCursorState.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
}
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Follow remote cursor')),
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: option);
|
||||
showCursorLockState.value = value;
|
||||
if (!showCursorEnabled) {
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: showCursorOption);
|
||||
showCursorState.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: showCursorOption);
|
||||
}
|
||||
}));
|
||||
}
|
||||
// follow remote window focus
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.isWayland &&
|
||||
versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||
pi.displays.length > 1 &&
|
||||
pi.currentDisplay != kAllDisplayValue &&
|
||||
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||
final option = 'follow-remote-window';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Follow remote window focus')),
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||
value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: option);
|
||||
}));
|
||||
}
|
||||
// zoom cursor
|
||||
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
|
||||
if (!isMobile &&
|
||||
@@ -418,6 +487,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
},
|
||||
));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
List<TToggleMenu> v = [];
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
|
||||
// show quality monitor
|
||||
final option = 'show-quality-monitor';
|
||||
v.add(TToggleMenu(
|
||||
@@ -442,10 +522,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Mute'))));
|
||||
}
|
||||
// file copy and paste
|
||||
// If the version is less than 1.2.4, file copy and paste is supported on Windows only.
|
||||
final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
|
||||
isWindows &&
|
||||
pi.platform == kPeerPlatformWindows;
|
||||
// If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
|
||||
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
|
||||
if (ffiModel.keyboard &&
|
||||
perms['file'] != false &&
|
||||
bind.mainHasFileClipboard() &&
|
||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
|
||||
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'enable-file-transfer';
|
||||
final value =
|
||||
@@ -512,8 +599,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
child: Text(translate('Show displays as individual windows'))));
|
||||
}
|
||||
|
||||
final screenList = await getScreenRectList();
|
||||
if (useTextureRender && pi.isSupportMultiDisplay && screenList.length > 1) {
|
||||
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
||||
if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) {
|
||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||
sessionId: ffi.sessionId) ==
|
||||
'Y';
|
||||
@@ -633,8 +720,8 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
|
||||
// swap key
|
||||
if (ffiModel.keyboard &&
|
||||
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
|
||||
(!Platform.isMacOS && pi.platform == kPeerPlatformMacOS))) {
|
||||
((isMacOS && pi.platform != kPeerPlatformMacOS) ||
|
||||
(!isMacOS && pi.platform == kPeerPlatformMacOS))) {
|
||||
final option = 'allow_swap_key';
|
||||
final value =
|
||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||
@@ -655,7 +742,7 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
var optionValue =
|
||||
bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? '';
|
||||
if (optionValue == '') {
|
||||
optionValue = bind.mainGetUserDefaultOption(key: 'reverse_mouse_wheel');
|
||||
optionValue = bind.mainGetUserDefaultOption(key: kKeyReverseMouseWheel);
|
||||
}
|
||||
onChanged(bool? value) async {
|
||||
if (value == null) return;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -21,9 +19,14 @@ const kKeyTranslateMode = 'translate';
|
||||
const String kPlatformAdditionsIsWayland = "is_wayland";
|
||||
const String kPlatformAdditionsHeadless = "headless";
|
||||
const String kPlatformAdditionsIsInstalled = "is_installed";
|
||||
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
|
||||
const String kPlatformAdditionsIddImpl = "idd_impl";
|
||||
const String kPlatformAdditionsRustDeskVirtualDisplays =
|
||||
"rustdesk_virtual_displays";
|
||||
const String kPlatformAdditionsAmyuniVirtualDisplays =
|
||||
"amyuni_virtual_displays";
|
||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||
const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl";
|
||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||
"supported_privacy_mode_impl";
|
||||
|
||||
const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
@@ -59,6 +62,7 @@ const String kWindowEventActiveSession = "active_session";
|
||||
const String kWindowEventActiveDisplaySession = "active_display_session";
|
||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
||||
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
|
||||
|
||||
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
||||
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
||||
@@ -68,8 +72,8 @@ const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||
const String kOptionOpenInWindows = "allow-open-in-windows";
|
||||
const String kOptionForceAlwaysRelay = "force-always-relay";
|
||||
const String kOptionViewOnly = "view-only";
|
||||
|
||||
const String kUniLinksPrefix = "rustdesk://";
|
||||
const String kUrlActionClose = "close";
|
||||
|
||||
const String kTabLabelHomePage = "Home";
|
||||
@@ -86,6 +90,7 @@ const String kKeyShowDisplaysAsIndividualWindows =
|
||||
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||
'use_all_my_displays_for_the_remote_session';
|
||||
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
||||
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
||||
|
||||
// the executable name of the portable version
|
||||
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
||||
@@ -113,20 +118,19 @@ const double kDefaultQuality = 50;
|
||||
const double kMaxQuality = 100;
|
||||
const double kMaxMoreQuality = 2000;
|
||||
|
||||
double kNewWindowOffset = Platform.isWindows
|
||||
double kNewWindowOffset = isWindows
|
||||
? 56.0
|
||||
: Platform.isLinux
|
||||
: isLinux
|
||||
? 50.0
|
||||
: Platform.isMacOS
|
||||
: isMacOS
|
||||
? 30.0
|
||||
: 50.0;
|
||||
|
||||
EdgeInsets get kDragToResizeAreaPadding =>
|
||||
!kUseCompatibleUiMode && Platform.isLinux
|
||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(5.0)
|
||||
: EdgeInsets.zero;
|
||||
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
|
||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(5.0)
|
||||
: EdgeInsets.zero;
|
||||
// https://en.wikipedia.org/wiki/Non-breaking_space
|
||||
const int $nbsp = 0x00A0;
|
||||
|
||||
@@ -150,9 +154,15 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
|
||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||
const kFullScreenEdgeSize = 0.0;
|
||||
const kMaximizeEdgeSize = 0.0;
|
||||
var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0;
|
||||
const kWindowBorderWidth = 1.0;
|
||||
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
||||
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||
final kWindowBorderWidth = isLinux ? 1.0 : 0.0;
|
||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||
const kFrameBorderRadius = 12.0;
|
||||
const kFrameClipRRectBorderRadius = 12.0;
|
||||
const kFrameBoxShadowBlurRadius = 32.0;
|
||||
const kFrameBoxShadowOffsetFocused = 4.0;
|
||||
const kFrameBoxShadowOffsetUnfocused = 2.0;
|
||||
|
||||
const kInvalidValueStr = 'InvalidValueStr';
|
||||
|
||||
@@ -222,6 +232,7 @@ const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
|
||||
const kRequestIgnoreBatteryOptimizations =
|
||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
||||
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
||||
const kAndroid13Notification = "android.permission.POST_NOTIFICATIONS";
|
||||
|
||||
/// Android channel invoke type key
|
||||
class AndroidChannel {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -20,6 +19,173 @@ import '../../common/widgets/autocomplete.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/button.dart';
|
||||
|
||||
class OnlineStatusWidget extends StatefulWidget {
|
||||
const OnlineStatusWidget({Key? key, this.onSvcStatusChanged})
|
||||
: super(key: key);
|
||||
|
||||
final VoidCallback? onSvcStatusChanged;
|
||||
|
||||
@override
|
||||
State<OnlineStatusWidget> createState() => _OnlineStatusWidgetState();
|
||||
}
|
||||
|
||||
/// State for the connection page.
|
||||
class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
|
||||
final _svcStopped = Get.find<RxBool>(tag: 'stop-service');
|
||||
final _svcIsUsingPublicServer = true.obs;
|
||||
Timer? _updateTimer;
|
||||
|
||||
double get em => 14.0;
|
||||
double? get height => bind.isIncomingOnly() ? null : em * 3;
|
||||
|
||||
void onUsePublicServerGuide() {
|
||||
const url = "https://rustdesk.com/pricing.html";
|
||||
canLaunchUrlString(url).then((can) {
|
||||
if (can) {
|
||||
launchUrlString(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateTimer = periodic_immediate(Duration(seconds: 1), () async {
|
||||
updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_updateTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isIncomingOnly = bind.isIncomingOnly();
|
||||
startServiceWidget() => Offstage(
|
||||
offstage: !_svcStopped.value,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await start_service(true);
|
||||
},
|
||||
child: Text(translate("Start service"),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline, fontSize: em)))
|
||||
.marginOnly(left: em),
|
||||
);
|
||||
|
||||
setupServerWidget() => Flexible(
|
||||
child: Offstage(
|
||||
offstage: !(!_svcStopped.value &&
|
||||
stateGlobal.svcStatus.value == SvcStatus.ready &&
|
||||
_svcIsUsingPublicServer.value),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(', ', style: TextStyle(fontSize: em)),
|
||||
Flexible(
|
||||
child: InkWell(
|
||||
onTap: onUsePublicServerGuide,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
translate('setup_server_tip'),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: em),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
basicWidget() => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: _svcStopped.value ||
|
||||
stateGlobal.svcStatus.value == SvcStatus.connecting
|
||||
? kColorWarn
|
||||
: (stateGlobal.svcStatus.value == SvcStatus.ready
|
||||
? Color.fromARGB(255, 50, 190, 166)
|
||||
: Color.fromARGB(255, 224, 79, 95)),
|
||||
),
|
||||
).marginSymmetric(horizontal: em),
|
||||
Container(
|
||||
width: isIncomingOnly ? 226 : null,
|
||||
child: _buildConnStatusMsg(),
|
||||
),
|
||||
// stop
|
||||
if (!isIncomingOnly) startServiceWidget(),
|
||||
// ready && public
|
||||
// No need to show the guide if is custom client.
|
||||
if (!isIncomingOnly) setupServerWidget(),
|
||||
],
|
||||
);
|
||||
|
||||
return Container(
|
||||
height: height,
|
||||
child: Obx(() => isIncomingOnly
|
||||
? Column(
|
||||
children: [
|
||||
basicWidget(),
|
||||
Align(
|
||||
child: startServiceWidget(),
|
||||
alignment: Alignment.centerLeft)
|
||||
.marginOnly(top: 2.0, left: 22.0),
|
||||
],
|
||||
)
|
||||
: basicWidget()),
|
||||
).paddingOnly(right: isIncomingOnly ? 8 : 0);
|
||||
}
|
||||
|
||||
_buildConnStatusMsg() {
|
||||
widget.onSvcStatusChanged?.call();
|
||||
return Text(
|
||||
_svcStopped.value
|
||||
? translate("Service is not running")
|
||||
: stateGlobal.svcStatus.value == SvcStatus.connecting
|
||||
? translate("connecting_status")
|
||||
: stateGlobal.svcStatus.value == SvcStatus.notReady
|
||||
? translate("not_ready_status")
|
||||
: translate('Ready'),
|
||||
style: TextStyle(fontSize: em),
|
||||
);
|
||||
}
|
||||
|
||||
updateStatus() async {
|
||||
final status =
|
||||
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
||||
final statusNum = status['status_num'] as int;
|
||||
final preStatus = stateGlobal.svcStatus.value;
|
||||
if (statusNum == 0) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
||||
} else if (statusNum == -1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
} else if (statusNum == 1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.ready;
|
||||
if (preStatus != SvcStatus.ready) {
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
}
|
||||
} else {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
}
|
||||
_svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection page for connecting to a remote peer.
|
||||
class ConnectionPage extends StatefulWidget {
|
||||
const ConnectionPage({Key? key}) : super(key: key);
|
||||
@@ -34,13 +200,8 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
/// Controller for the id input bar.
|
||||
final _idController = IDTextEditingController();
|
||||
|
||||
Timer? _updateTimer;
|
||||
|
||||
final RxBool _idInputFocused = false.obs;
|
||||
|
||||
var svcStopped = Get.find<RxBool>(tag: 'stop-service');
|
||||
var svcIsUsingPublicServer = true.obs;
|
||||
|
||||
bool isWindowMinimized = false;
|
||||
List<Peer> peers = [];
|
||||
|
||||
@@ -60,9 +221,6 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
}
|
||||
}();
|
||||
}
|
||||
_updateTimer = periodic_immediate(Duration(seconds: 1), () async {
|
||||
updateStatus();
|
||||
});
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
@@ -70,7 +228,6 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
@override
|
||||
void dispose() {
|
||||
_idController.dispose();
|
||||
_updateTimer?.cancel();
|
||||
windowManager.removeListener(this);
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
@@ -87,7 +244,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
if (eventName == 'minimize') {
|
||||
isWindowMinimized = true;
|
||||
} else if (eventName == 'maximize' || eventName == 'restore') {
|
||||
if (isWindowMinimized && Platform.isWindows) {
|
||||
if (isWindowMinimized && isWindows) {
|
||||
// windows can't update when minimized.
|
||||
Get.forceAppUpdate();
|
||||
}
|
||||
@@ -105,7 +262,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
void onWindowLeaveFullScreen() {
|
||||
// Restore edge border to default edge size.
|
||||
stateGlobal.resizeEdgeSize.value =
|
||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
|
||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -116,6 +273,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -131,8 +289,8 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
Expanded(child: PeerTabPage()),
|
||||
],
|
||||
).paddingOnly(left: 12.0)),
|
||||
const Divider(height: 1),
|
||||
buildStatus()
|
||||
if (!isOutgoingOnly) const Divider(height: 1),
|
||||
if (!isOutgoingOnly) OnlineStatusWidget()
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -214,6 +372,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
platform: '',
|
||||
tags: [],
|
||||
hash: '',
|
||||
password: '',
|
||||
forceAlwaysRelay: false,
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
@@ -289,6 +448,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
onChanged: (v) {
|
||||
_idController.id = v;
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
onConnect();
|
||||
},
|
||||
));
|
||||
},
|
||||
onSelected: (option) {
|
||||
@@ -383,111 +545,4 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600), child: w);
|
||||
}
|
||||
|
||||
Widget buildStatus() {
|
||||
final em = 14.0;
|
||||
return Container(
|
||||
height: 3 * em,
|
||||
child: Obx(() => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: svcStopped.value ||
|
||||
stateGlobal.svcStatus.value == SvcStatus.connecting
|
||||
? kColorWarn
|
||||
: (stateGlobal.svcStatus.value == SvcStatus.ready
|
||||
? Color.fromARGB(255, 50, 190, 166)
|
||||
: Color.fromARGB(255, 224, 79, 95)),
|
||||
),
|
||||
).marginSymmetric(horizontal: em),
|
||||
Text(
|
||||
svcStopped.value
|
||||
? translate("Service is not running")
|
||||
: stateGlobal.svcStatus.value == SvcStatus.connecting
|
||||
? translate("connecting_status")
|
||||
: stateGlobal.svcStatus.value == SvcStatus.notReady
|
||||
? translate("not_ready_status")
|
||||
: translate('Ready'),
|
||||
style: TextStyle(fontSize: em)),
|
||||
// stop
|
||||
Offstage(
|
||||
offstage: !svcStopped.value,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await start_service(true);
|
||||
},
|
||||
child: Text(translate("Start service"),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: em)))
|
||||
.marginOnly(left: em),
|
||||
),
|
||||
// ready && public
|
||||
Flexible(
|
||||
child: Offstage(
|
||||
offstage: !(!svcStopped.value &&
|
||||
stateGlobal.svcStatus.value == SvcStatus.ready &&
|
||||
svcIsUsingPublicServer.value),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(', ', style: TextStyle(fontSize: em)),
|
||||
Flexible(
|
||||
child: InkWell(
|
||||
onTap: onUsePublicServerGuide,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
translate('setup_server_tip'),
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: em),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
void onUsePublicServerGuide() {
|
||||
const url = "https://rustdesk.com/pricing.html";
|
||||
canLaunchUrlString(url).then((can) {
|
||||
if (can) {
|
||||
launchUrlString(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus() async {
|
||||
final status =
|
||||
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
||||
final statusNum = status['status_num'] as int;
|
||||
final preStatus = stateGlobal.svcStatus.value;
|
||||
if (statusNum == 0) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
||||
} else if (statusNum == -1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
} else if (statusNum == 1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.ready;
|
||||
if (preStatus != SvcStatus.ready) {
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
}
|
||||
} else {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
}
|
||||
svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:window_size/window_size.dart' as window_size;
|
||||
|
||||
import '../widgets/button.dart';
|
||||
@@ -50,50 +51,120 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
Timer? _updateTimer;
|
||||
bool isCardClosed = false;
|
||||
|
||||
final RxBool _editHover = false.obs;
|
||||
|
||||
final GlobalKey _childKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final isIncomingOnly = bind.isIncomingOnly();
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildLeftPane(context),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(
|
||||
child: buildRightPane(context),
|
||||
),
|
||||
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
||||
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLeftPane(BuildContext context) {
|
||||
final isIncomingOnly = bind.isIncomingOnly();
|
||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||
final children = <Widget>[
|
||||
if (!isOutgoingOnly) buildPresetPasswordWarning(),
|
||||
if (bind.isCustomClient())
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: loadPowered(context),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: loadLogo(),
|
||||
),
|
||||
buildTip(context),
|
||||
if (!isOutgoingOnly) buildIDBoard(context),
|
||||
if (!isOutgoingOnly) buildPasswordBoard(context),
|
||||
FutureBuilder<Widget>(
|
||||
future: buildHelpCards(),
|
||||
builder: (_, data) {
|
||||
if (data.hasData) {
|
||||
if (isIncomingOnly) {
|
||||
if (isInHomePage()) {
|
||||
Future.delayed(Duration(milliseconds: 300), () {
|
||||
_updateWindowSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
return data.data!;
|
||||
} else {
|
||||
return const Offstage();
|
||||
}
|
||||
},
|
||||
),
|
||||
buildPluginEntry(),
|
||||
];
|
||||
if (isIncomingOnly) {
|
||||
children.addAll([
|
||||
Divider(),
|
||||
OnlineStatusWidget(
|
||||
onSvcStatusChanged: () {
|
||||
if (isInHomePage()) {
|
||||
Future.delayed(Duration(milliseconds: 300), () {
|
||||
_updateWindowSize();
|
||||
});
|
||||
}
|
||||
},
|
||||
).marginOnly(bottom: 6, right: 6)
|
||||
]);
|
||||
}
|
||||
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
||||
return ChangeNotifierProvider.value(
|
||||
value: gFFI.serverModel,
|
||||
child: Container(
|
||||
width: 200,
|
||||
width: isIncomingOnly ? 280.0 : 200.0,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: DesktopScrollWrapper(
|
||||
scrollController: _leftPaneScrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: _leftPaneScrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
buildTip(context),
|
||||
buildIDBoard(context),
|
||||
buildPasswordBoard(context),
|
||||
FutureBuilder<Widget>(
|
||||
future: buildHelpCards(),
|
||||
builder: (_, data) {
|
||||
if (data.hasData) {
|
||||
return data.data!;
|
||||
} else {
|
||||
return const Offstage();
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
controller: _leftPaneScrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
key: _childKey,
|
||||
children: children,
|
||||
),
|
||||
buildPluginEntry()
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isOutgoingOnly)
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: 12,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: InkWell(
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.settings,
|
||||
color: _editHover.value
|
||||
? textColor
|
||||
: Colors.grey.withOpacity(0.5),
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
onTap: () => {
|
||||
if (DesktopSettingPage.tabKeys.isNotEmpty)
|
||||
{
|
||||
DesktopSettingPage.switch2page(
|
||||
DesktopSettingPage.tabKeys[0])
|
||||
}
|
||||
},
|
||||
onHover: (value) => _editHover.value = value,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -180,19 +251,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
RxBool hover = false.obs;
|
||||
return InkWell(
|
||||
onTap: DesktopTabPage.onAddSetting,
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Tooltip(
|
||||
message: translate('Settings'),
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
)),
|
||||
child: Tooltip(
|
||||
message: translate('Settings'),
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: Theme.of(context).colorScheme.background,
|
||||
child: Icon(
|
||||
Icons.more_vert_outlined,
|
||||
size: 20,
|
||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onHover: (value) => hover.value = value,
|
||||
@@ -253,34 +325,38 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
AnimatedRotationWidget(
|
||||
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)))),
|
||||
child: Tooltip(
|
||||
message: translate('Refresh Password'),
|
||||
child: Obx(() => RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: refreshHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
))),
|
||||
),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
).marginOnly(right: 8, top: 4),
|
||||
InkWell(
|
||||
child: Obx(
|
||||
() => Tooltip(
|
||||
message: translate('Change Password'),
|
||||
child: Icon(
|
||||
if (!bind.isDisableSettings())
|
||||
InkWell(
|
||||
child: Tooltip(
|
||||
message: translate('Change Password'),
|
||||
child: Obx(
|
||||
() => Icon(
|
||||
Icons.edit,
|
||||
color: editHover.value
|
||||
? textColor
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
)).marginOnly(right: 8, top: 4),
|
||||
).marginOnly(right: 8, top: 4),
|
||||
),
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(
|
||||
SettingsTabKey.safety),
|
||||
onHover: (value) => editHover.value = value,
|
||||
),
|
||||
onTap: () => DesktopSettingPage.switch2page(1),
|
||||
onHover: (value) => editHover.value = value,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -293,6 +369,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
|
||||
buildTip(BuildContext context) {
|
||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
|
||||
@@ -300,29 +377,43 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
translate("Your Desktop"),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
// style: TextStyle(
|
||||
// // color: MyTheme.color(context).text,
|
||||
// fontWeight: FontWeight.normal,
|
||||
// fontSize: 19),
|
||||
Column(
|
||||
children: [
|
||||
if (!isOutgoingOnly)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
translate("Your Desktop"),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 10.0,
|
||||
),
|
||||
Text(
|
||||
translate("desk_tip"),
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
if (!isOutgoingOnly)
|
||||
Text(
|
||||
translate("desk_tip"),
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
if (isOutgoingOnly)
|
||||
Text(
|
||||
translate("outgoing_only_desk_tip"),
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Widget> buildHelpCards() async {
|
||||
if (updateUrl.isNotEmpty && !isCardClosed) {
|
||||
if (!bind.isCustomClient() &&
|
||||
updateUrl.isNotEmpty &&
|
||||
!isCardClosed &&
|
||||
bind.mainUriPrefixSync().contains('rustdesk')) {
|
||||
return buildInstallCard(
|
||||
"Status",
|
||||
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
|
||||
@@ -334,9 +425,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
if (systemError.isNotEmpty) {
|
||||
return buildInstallCard("", systemError, "", () {});
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
|
||||
if (isWindows && !bind.isDisableInstallation()) {
|
||||
if (!bind.mainIsInstalled()) {
|
||||
return buildInstallCard("", "install_tip", "Install", () async {
|
||||
return buildInstallCard(
|
||||
"", bind.isOutgoingOnly() ? "" : "install_tip", "Install",
|
||||
() async {
|
||||
await rustDeskWinManager.closeAllSubWindows();
|
||||
bind.mainGotoInstall();
|
||||
});
|
||||
@@ -348,8 +442,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
bind.mainUpdateMe();
|
||||
});
|
||||
}
|
||||
} else if (Platform.isMacOS) {
|
||||
if (!bind.mainIsCanScreenRecording(prompt: false)) {
|
||||
} else if (isMacOS) {
|
||||
if (!(bind.isOutgoingOnly() ||
|
||||
bind.mainIsCanScreenRecording(prompt: false))) {
|
||||
return buildInstallCard("Permissions", "config_screen", "Configure",
|
||||
() async {
|
||||
bind.mainIsCanScreenRecording(prompt: true);
|
||||
@@ -383,7 +478,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
// watchIsCanRecordAudio = true;
|
||||
// });
|
||||
// }
|
||||
} else if (Platform.isLinux) {
|
||||
} else if (isLinux) {
|
||||
if (bind.isOutgoingOnly()) {
|
||||
return Container();
|
||||
}
|
||||
final LinuxCards = <Widget>[];
|
||||
if (bind.isSelinuxEnforcing()) {
|
||||
// Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
|
||||
@@ -422,6 +520,21 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (bind.isIncomingOnly()) {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
SystemNavigator.pop(); // Close the application
|
||||
// https://github.com/flutter/flutter/issues/66631
|
||||
if (isWindows) {
|
||||
exit(0);
|
||||
}
|
||||
},
|
||||
child: Text(translate('Quit')),
|
||||
),
|
||||
).marginAll(14);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
@@ -450,7 +563,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: marginTop),
|
||||
margin: EdgeInsets.fromLTRB(
|
||||
0, marginTop, 0, bind.isIncomingOnly() ? marginTop : 0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -478,14 +592,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
]
|
||||
: <Widget>[]) +
|
||||
<Widget>[
|
||||
Text(
|
||||
translate(content),
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 13),
|
||||
).marginOnly(bottom: 20)
|
||||
if (content.isNotEmpty)
|
||||
Text(
|
||||
translate(content),
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 13),
|
||||
).marginOnly(bottom: 20)
|
||||
] +
|
||||
(btnText.isNotEmpty
|
||||
? <Widget>[
|
||||
@@ -582,7 +697,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
}
|
||||
if (watchIsCanRecordAudio) {
|
||||
if (Platform.isMacOS) {
|
||||
if (isMacOS) {
|
||||
Future.microtask(() async {
|
||||
if ((await osxCanRecordAudio() ==
|
||||
PermissionAuthorizeType.authorized)) {
|
||||
@@ -642,6 +757,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
isFileTransfer: call.arguments['isFileTransfer'],
|
||||
isTcpTunneling: call.arguments['isTcpTunneling'],
|
||||
isRDP: call.arguments['isRDP'],
|
||||
password: call.arguments['password'],
|
||||
forceRelay: call.arguments['forceRelay'],
|
||||
);
|
||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||
@@ -665,9 +781,35 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
await rustDeskWinManager.openMonitorSession(
|
||||
windowId, peerId, display, displayCount, screenRect);
|
||||
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||
final windowId = int.tryParse(call.arguments);
|
||||
if (windowId != null) {
|
||||
return jsonEncode(
|
||||
await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
|
||||
}
|
||||
}
|
||||
});
|
||||
_uniLinksSubscription = listenUniLinks();
|
||||
|
||||
if (bind.isIncomingOnly()) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_updateWindowSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_updateWindowSize() {
|
||||
RenderObject? renderObject = _childKey.currentContext?.findRenderObject();
|
||||
if (renderObject == null) {
|
||||
return;
|
||||
}
|
||||
if (renderObject is RenderBox) {
|
||||
final size = renderObject.size;
|
||||
if (size != imcomingOnlyHomeSize) {
|
||||
imcomingOnlyHomeSize = size;
|
||||
windowManager.setSize(getIncomingOnlyHomeSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
@@ -36,34 +37,57 @@ const double _kTitleFontSize = 20;
|
||||
const double _kContentFontSize = 15;
|
||||
const Color _accentColor = MyTheme.accent;
|
||||
const String _kSettingPageControllerTag = 'settingPageController';
|
||||
const String _kSettingPageIndexTag = 'settingPageIndex';
|
||||
const int _kPageCount = 6;
|
||||
const String _kSettingPageTabKeyTag = 'settingPageTabKey';
|
||||
|
||||
class _TabInfo {
|
||||
late final SettingsTabKey key;
|
||||
late final String label;
|
||||
late final IconData unselected;
|
||||
late final IconData selected;
|
||||
_TabInfo(this.label, this.unselected, this.selected);
|
||||
_TabInfo(this.key, this.label, this.unselected, this.selected);
|
||||
}
|
||||
|
||||
enum SettingsTabKey {
|
||||
general,
|
||||
safety,
|
||||
network,
|
||||
display,
|
||||
plugin,
|
||||
account,
|
||||
about,
|
||||
}
|
||||
|
||||
class DesktopSettingPage extends StatefulWidget {
|
||||
final int initialPage;
|
||||
final SettingsTabKey initialTabkey;
|
||||
static final List<SettingsTabKey> tabKeys = [
|
||||
SettingsTabKey.general,
|
||||
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
|
||||
SettingsTabKey.safety,
|
||||
if (!bind.isDisableSettings()) SettingsTabKey.network,
|
||||
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
||||
SettingsTabKey.plugin,
|
||||
if (!bind.isDisableAccount()) SettingsTabKey.account,
|
||||
SettingsTabKey.about,
|
||||
];
|
||||
|
||||
const DesktopSettingPage({Key? key, required this.initialPage})
|
||||
: super(key: key);
|
||||
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
|
||||
|
||||
static void switch2page(int page) {
|
||||
if (page >= _kPageCount) return;
|
||||
static void switch2page(SettingsTabKey page) {
|
||||
try {
|
||||
int index = tabKeys.indexOf(page);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
||||
DesktopTabPage.onAddSetting(initialPage: page);
|
||||
PageController controller = Get.find(tag: _kSettingPageControllerTag);
|
||||
RxInt selectedIndex = Get.find(tag: _kSettingPageIndexTag);
|
||||
selectedIndex.value = page;
|
||||
controller.jumpToPage(page);
|
||||
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
|
||||
selected.value = page;
|
||||
controller.jumpToPage(index);
|
||||
} else {
|
||||
DesktopTabPage.onAddSetting(initialPage: page);
|
||||
}
|
||||
@@ -76,7 +100,7 @@ class DesktopSettingPage extends StatefulWidget {
|
||||
class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||
late PageController controller;
|
||||
late RxInt selectedIndex;
|
||||
late Rx<SettingsTabKey> selectedTab;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -84,14 +108,20 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedIndex =
|
||||
(widget.initialPage < _kPageCount ? widget.initialPage : 0).obs;
|
||||
Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag);
|
||||
controller = PageController(initialPage: widget.initialPage);
|
||||
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
|
||||
if (initialIndex == -1) {
|
||||
initialIndex = 0;
|
||||
}
|
||||
selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs;
|
||||
Get.put<Rx<SettingsTabKey>>(selectedTab, tag: _kSettingPageTabKeyTag);
|
||||
controller = PageController(initialPage: initialIndex);
|
||||
Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
|
||||
controller.addListener(() {
|
||||
if (controller.page != null) {
|
||||
selectedIndex.value = controller.page!.toInt();
|
||||
int page = controller.page!.toInt();
|
||||
if (page < DesktopSettingPage.tabKeys.length) {
|
||||
selectedTab.value = DesktopSettingPage.tabKeys[page];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -100,23 +130,42 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
Get.delete<PageController>(tag: _kSettingPageControllerTag);
|
||||
Get.delete<RxInt>(tag: _kSettingPageIndexTag);
|
||||
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
|
||||
}
|
||||
|
||||
List<_TabInfo> _settingTabs() {
|
||||
final List<_TabInfo> settingTabs = <_TabInfo>[
|
||||
_TabInfo('General', Icons.settings_outlined, Icons.settings),
|
||||
_TabInfo('Security', Icons.enhanced_encryption_outlined,
|
||||
Icons.enhanced_encryption),
|
||||
_TabInfo('Network', Icons.link_outlined, Icons.link),
|
||||
_TabInfo(
|
||||
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows),
|
||||
_TabInfo('Account', Icons.person_outline, Icons.person),
|
||||
_TabInfo('About', Icons.info_outline, Icons.info)
|
||||
];
|
||||
if (bind.pluginFeatureIsEnabled()) {
|
||||
settingTabs.insert(
|
||||
4, _TabInfo('Plugin', Icons.extension_outlined, Icons.extension));
|
||||
final List<_TabInfo> settingTabs = <_TabInfo>[];
|
||||
for (final tab in DesktopSettingPage.tabKeys) {
|
||||
switch (tab) {
|
||||
case SettingsTabKey.general:
|
||||
settingTabs.add(_TabInfo(
|
||||
tab, 'General', Icons.settings_outlined, Icons.settings));
|
||||
break;
|
||||
case SettingsTabKey.safety:
|
||||
settingTabs.add(_TabInfo(tab, 'Security',
|
||||
Icons.enhanced_encryption_outlined, Icons.enhanced_encryption));
|
||||
break;
|
||||
case SettingsTabKey.network:
|
||||
settingTabs
|
||||
.add(_TabInfo(tab, 'Network', Icons.link_outlined, Icons.link));
|
||||
break;
|
||||
case SettingsTabKey.display:
|
||||
settingTabs.add(_TabInfo(tab, 'Display',
|
||||
Icons.desktop_windows_outlined, Icons.desktop_windows));
|
||||
break;
|
||||
case SettingsTabKey.plugin:
|
||||
settingTabs.add(_TabInfo(
|
||||
tab, 'Plugin', Icons.extension_outlined, Icons.extension));
|
||||
break;
|
||||
case SettingsTabKey.account:
|
||||
settingTabs.add(
|
||||
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
|
||||
break;
|
||||
case SettingsTabKey.about:
|
||||
settingTabs
|
||||
.add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return settingTabs;
|
||||
}
|
||||
@@ -124,15 +173,14 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
List<Widget> _children() {
|
||||
final children = [
|
||||
_General(),
|
||||
_Safety(),
|
||||
_Network(),
|
||||
_Display(),
|
||||
_Account(),
|
||||
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(),
|
||||
if (!bind.isDisableSettings()) _Network(),
|
||||
if (!bind.isIncomingOnly()) _Display(),
|
||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
||||
_Plugin(),
|
||||
if (!bind.isDisableAccount()) _Account(),
|
||||
_About(),
|
||||
];
|
||||
if (bind.pluginFeatureIsEnabled()) {
|
||||
children.insert(4, _Plugin());
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -197,26 +245,26 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
child: ListView(
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
children: tabs
|
||||
.asMap()
|
||||
.entries
|
||||
.map((tab) => _listItem(tab: tab.value, index: tab.key))
|
||||
.toList(),
|
||||
children: tabs.map((tab) => _listItem(tab: tab)).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _listItem({required _TabInfo tab, required int index}) {
|
||||
Widget _listItem({required _TabInfo tab}) {
|
||||
return Obx(() {
|
||||
bool selected = index == selectedIndex.value;
|
||||
bool selected = tab.key == selectedTab.value;
|
||||
return SizedBox(
|
||||
width: _kTabWidth,
|
||||
height: _kTabHeight,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (selectedIndex.value != index) {
|
||||
if (selectedTab.value != tab.key) {
|
||||
int index = DesktopSettingPage.tabKeys.indexOf(tab.key);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
controller.jumpToPage(index);
|
||||
}
|
||||
selectedIndex.value = index;
|
||||
selectedTab.value = tab.key;
|
||||
},
|
||||
child: Row(children: [
|
||||
Container(
|
||||
@@ -270,6 +318,7 @@ class _GeneralState extends State<_General> {
|
||||
hwcodec(),
|
||||
audio(context),
|
||||
record(context),
|
||||
WaylandCard(),
|
||||
_Card(title: 'Language', children: [language()]),
|
||||
other()
|
||||
],
|
||||
@@ -303,6 +352,10 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget service() {
|
||||
if (bind.isOutgoingOnly()) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
return _Card(title: 'Service', children: [
|
||||
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
|
||||
() async {
|
||||
@@ -318,31 +371,34 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget other() {
|
||||
final children = [
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
'enable-confirm-closing-tabs',
|
||||
isServer: false),
|
||||
final children = <Widget>[
|
||||
if (!bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
'enable-confirm-closing-tabs',
|
||||
isServer: false),
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
|
||||
wallpaper(),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Open connection in new tab',
|
||||
kOptionOpenNewConnInTabs,
|
||||
isServer: false,
|
||||
),
|
||||
if (!bind.isIncomingOnly()) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Open connection in new tab',
|
||||
kOptionOpenNewConnInTabs,
|
||||
isServer: false,
|
||||
),
|
||||
// though this is related to GUI, but opengl problem affects all users, so put in config rather than local
|
||||
Tooltip(
|
||||
message: translate('software_render_tip'),
|
||||
child: _OptionCheckBox(context, "Always use software rendering",
|
||||
'allow-always-software-render'),
|
||||
),
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Check for software update on startup',
|
||||
'enable-check-update',
|
||||
isServer: false,
|
||||
)
|
||||
],
|
||||
];
|
||||
// though this is related to GUI, but opengl problem affects all users, so put in config rather than local
|
||||
children.add(Tooltip(
|
||||
message: translate('software_render_tip'),
|
||||
child: _OptionCheckBox(context, "Always use software rendering",
|
||||
'allow-always-software-render'),
|
||||
));
|
||||
children.add(_OptionCheckBox(
|
||||
context,
|
||||
'Check for software update on startup',
|
||||
'enable-check-update',
|
||||
isServer: false,
|
||||
));
|
||||
if (bind.mainShowOption(key: 'allow-linux-headless')) {
|
||||
children.add(_OptionCheckBox(
|
||||
context, 'Allow linux headless', 'allow-linux-headless'));
|
||||
@@ -351,6 +407,10 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget wallpaper() {
|
||||
if (bind.isOutgoingOnly()) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
return futureBuilder(future: () async {
|
||||
final support = await bind.mainSupportRemoveWallpaper();
|
||||
return support;
|
||||
@@ -388,48 +448,30 @@ class _GeneralState extends State<_General> {
|
||||
|
||||
Widget hwcodec() {
|
||||
final hwcodec = bind.mainHasHwcodec();
|
||||
final gpucodec = bind.mainHasGpucodec();
|
||||
final vram = bind.mainHasVram();
|
||||
return Offstage(
|
||||
offstage: !(hwcodec || gpucodec),
|
||||
offstage: !(hwcodec || vram),
|
||||
child: _Card(title: 'Hardware Codec', children: [
|
||||
_OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec')
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Enable hardware codec',
|
||||
'enable-hwcodec',
|
||||
update: () {
|
||||
if (mainGetBoolOptionSync('enable-hwcodec')) {
|
||||
bind.mainCheckHwcodec();
|
||||
}
|
||||
},
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget audio(BuildContext context) {
|
||||
String getDefault() {
|
||||
if (Platform.isWindows) return translate('System Sound');
|
||||
return '';
|
||||
if (bind.isOutgoingOnly()) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
Future<String> getValue() async {
|
||||
String device = await bind.mainGetOption(key: 'audio-input');
|
||||
if (device.isNotEmpty) {
|
||||
return device;
|
||||
} else {
|
||||
return getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
setDevice(String device) {
|
||||
if (device == getDefault()) device = '';
|
||||
bind.mainSetOption(key: 'audio-input', value: device);
|
||||
}
|
||||
|
||||
return futureBuilder(future: () async {
|
||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||
if (Platform.isWindows) {
|
||||
devices.insert(0, translate('System Sound'));
|
||||
}
|
||||
String current = await getValue();
|
||||
return {'devices': devices, 'current': current};
|
||||
}(), hasData: (data) {
|
||||
String currentDevice = data['current'];
|
||||
List<String> devices = data['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
return const Offstage();
|
||||
}
|
||||
return AudioInput(builder: (devices, currentDevice, setDevice) {
|
||||
return _Card(title: 'Audio Input Device', children: [
|
||||
...devices.map((device) => _Radio<String>(context,
|
||||
value: device,
|
||||
@@ -444,42 +486,72 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget record(BuildContext context) {
|
||||
final showRootDir = isWindows && bind.mainIsInstalled();
|
||||
return futureBuilder(future: () async {
|
||||
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
|
||||
String user_dir = await bind.mainVideoSaveDirectory(root: false);
|
||||
String root_dir =
|
||||
showRootDir ? await bind.mainVideoSaveDirectory(root: true) : '';
|
||||
bool user_dir_exists = await Directory(user_dir).exists();
|
||||
bool root_dir_exists =
|
||||
showRootDir ? await Directory(root_dir).exists() : false;
|
||||
// canLaunchUrl blocked on windows portable, user SYSTEM
|
||||
return {'dir': defaultDirectory, 'canlaunch': true};
|
||||
return {
|
||||
'user_dir': user_dir,
|
||||
'root_dir': root_dir,
|
||||
'user_dir_exists': user_dir_exists,
|
||||
'root_dir_exists': root_dir_exists,
|
||||
};
|
||||
}(), hasData: (data) {
|
||||
Map<String, dynamic> map = data as Map<String, dynamic>;
|
||||
String dir = map['dir']!;
|
||||
String customDirectory =
|
||||
bind.mainGetOptionSync(key: 'video-save-directory');
|
||||
if (customDirectory.isNotEmpty) {
|
||||
dir = customDirectory;
|
||||
}
|
||||
bool canlaunch = map['canlaunch']! as bool;
|
||||
|
||||
String user_dir = map['user_dir']!;
|
||||
String root_dir = map['root_dir']!;
|
||||
bool root_dir_exists = map['root_dir_exists']!;
|
||||
bool user_dir_exists = map['user_dir_exists']!;
|
||||
return _Card(title: 'Recording', children: [
|
||||
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
||||
'allow-auto-record-incoming'),
|
||||
if (showRootDir)
|
||||
Row(
|
||||
children: [
|
||||
Text('${translate("Incoming")}:'),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: root_dir_exists
|
||||
? () => launchUrl(Uri.file(root_dir))
|
||||
: null,
|
||||
child: Text(
|
||||
root_dir,
|
||||
softWrap: true,
|
||||
style: root_dir_exists
|
||||
? const TextStyle(
|
||||
decoration: TextDecoration.underline)
|
||||
: null,
|
||||
)).marginOnly(left: 10),
|
||||
),
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin),
|
||||
Row(
|
||||
children: [
|
||||
Text('${translate("Directory")}:'),
|
||||
Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: canlaunch ? () => launchUrl(Uri.file(dir)) : null,
|
||||
onTap: user_dir_exists
|
||||
? () => launchUrl(Uri.file(user_dir))
|
||||
: null,
|
||||
child: Text(
|
||||
dir,
|
||||
user_dir,
|
||||
softWrap: true,
|
||||
style:
|
||||
const TextStyle(decoration: TextDecoration.underline),
|
||||
style: user_dir_exists
|
||||
? const TextStyle(decoration: TextDecoration.underline)
|
||||
: null,
|
||||
)).marginOnly(left: 10),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
String? initialDirectory;
|
||||
if (await Directory.fromUri(Uri.directory(dir))
|
||||
if (await Directory.fromUri(Uri.directory(user_dir))
|
||||
.exists()) {
|
||||
initialDirectory = dir;
|
||||
initialDirectory = user_dir;
|
||||
}
|
||||
String? selectedDirectory = await FilePicker.platform
|
||||
.getDirectoryPath(initialDirectory: initialDirectory);
|
||||
@@ -690,7 +762,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
_OptionCheckBox(
|
||||
context, 'Enable recording session', 'enable-record-session',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
if (Platform.isWindows)
|
||||
if (isWindows)
|
||||
_OptionCheckBox(
|
||||
context, 'Enable blocking user input', 'enable-block-input',
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
@@ -819,6 +891,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
...directIp(context),
|
||||
whitelist(),
|
||||
...autoDisconnect(context),
|
||||
if (bind.mainIsInstalled())
|
||||
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
|
||||
'allow-only-conn-window-open',
|
||||
reverse: false, enabled: enabled),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -830,7 +906,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
bool value = bind.mainIsShareRdp();
|
||||
return Offstage(
|
||||
offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()),
|
||||
offstage: !(isWindows && bind.mainIsInstalled()),
|
||||
child: GestureDetector(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -1088,7 +1164,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
child: Column(children: [
|
||||
server(enabled),
|
||||
_Card(title: 'Proxy', children: [
|
||||
_Button('Socks5 Proxy', changeSocks5Proxy,
|
||||
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
||||
enabled: enabled),
|
||||
]),
|
||||
]),
|
||||
@@ -1622,7 +1698,7 @@ class _AboutState extends State<_About> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Copyright © 2023 Purslane Ltd.\n$license',
|
||||
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
@@ -1780,14 +1856,80 @@ Widget _Radio<T>(BuildContext context,
|
||||
);
|
||||
}
|
||||
|
||||
class WaylandCard extends StatefulWidget {
|
||||
const WaylandCard({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<WaylandCard> createState() => _WaylandCardState();
|
||||
}
|
||||
|
||||
class _WaylandCardState extends State<WaylandCard> {
|
||||
final restoreTokenKey = 'wayland-restore-token';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return futureBuilder(
|
||||
future: bind.mainHandleWaylandScreencastRestoreToken(
|
||||
key: restoreTokenKey, value: "get"),
|
||||
hasData: (restoreToken) {
|
||||
final children = [
|
||||
if (restoreToken.isNotEmpty)
|
||||
_buildClearScreenSelection(context, restoreToken),
|
||||
];
|
||||
return Offstage(
|
||||
offstage: children.isEmpty,
|
||||
child: _Card(title: 'Wayland', children: children),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClearScreenSelection(BuildContext context, String restoreToken) {
|
||||
onConfirm() async {
|
||||
final msg = await bind.mainHandleWaylandScreencastRestoreToken(
|
||||
key: restoreTokenKey, value: "clear");
|
||||
gFFI.dialogManager.dismissAll();
|
||||
if (msg.isNotEmpty) {
|
||||
msgBox(gFFI.sessionId, 'custom-nocancel', 'Error', msg, '',
|
||||
gFFI.dialogManager);
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
showConfirmMsgBox() => msgBoxCommon(
|
||||
gFFI.dialogManager,
|
||||
'Confirmation',
|
||||
Text(
|
||||
translate('confirm_clear_Wayland_screen_selection_tip'),
|
||||
),
|
||||
[
|
||||
dialogButton('OK', onPressed: onConfirm),
|
||||
dialogButton('Cancel',
|
||||
onPressed: () => gFFI.dialogManager.dismissAll())
|
||||
]);
|
||||
|
||||
return _Button(
|
||||
'Clear Wayland screen selection',
|
||||
showConfirmMsgBox,
|
||||
tip: 'clear_Wayland_screen_selection_tip',
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.error.withOpacity(0.75)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
Widget _Button(String label, Function() onPressed,
|
||||
{bool enabled = true, String? tip}) {
|
||||
{bool enabled = true, String? tip, ButtonStyle? style}) {
|
||||
var button = ElevatedButton(
|
||||
onPressed: enabled ? onPressed : null,
|
||||
child: Text(
|
||||
translate(label),
|
||||
).marginSymmetric(horizontal: 15),
|
||||
style: style,
|
||||
);
|
||||
StatefulWidget child;
|
||||
if (tip == null) {
|
||||
@@ -2004,7 +2146,12 @@ void changeSocks5Proxy() async {
|
||||
password = pwdController.text.trim();
|
||||
|
||||
if (proxy.isNotEmpty) {
|
||||
proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy));
|
||||
String domainPort = proxy;
|
||||
if (domainPort.contains('://')) {
|
||||
domainPort = domainPort.split('://')[1];
|
||||
}
|
||||
proxyMsg = translate(await bind.mainTestIfValidServer(
|
||||
server: domainPort, testWithProxy: false));
|
||||
if (proxyMsg.isEmpty) {
|
||||
// ignore
|
||||
} else {
|
||||
@@ -2018,7 +2165,7 @@ void changeSocks5Proxy() async {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Socks5 Proxy')),
|
||||
title: Text(translate('Socks5/Http(s) Proxy')),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
@@ -2027,15 +2174,35 @@ void changeSocks5Proxy() async {
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
'${translate("Hostname")}:',
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10)),
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('Server'),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 0),
|
||||
message: translate("default_proxy_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
)).marginOnly(right: 10),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
|
||||
),
|
||||
controller: proxyController,
|
||||
autofocus: true,
|
||||
),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@@ -18,7 +17,8 @@ class DesktopTabPage extends StatefulWidget {
|
||||
@override
|
||||
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
||||
|
||||
static void onAddSetting({int initialPage = 0}) {
|
||||
static void onAddSetting(
|
||||
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
||||
try {
|
||||
DesktopTabController tabController = Get.find();
|
||||
tabController.add(TabInfo(
|
||||
@@ -28,7 +28,7 @@ class DesktopTabPage extends StatefulWidget {
|
||||
unselectedIcon: Icons.build_outlined,
|
||||
page: DesktopSettingPage(
|
||||
key: const ValueKey(kTabLabelSettingPage),
|
||||
initialPage: initialPage,
|
||||
initialTabkey: initialPage,
|
||||
)));
|
||||
} catch (e) {
|
||||
debugPrintStack(label: '$e');
|
||||
@@ -53,6 +53,17 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
page: DesktopHomePage(
|
||||
key: const ValueKey(kTabLabelHomePage),
|
||||
)));
|
||||
if (bind.isIncomingOnly()) {
|
||||
tabController.onSelected = (key) {
|
||||
if (key == kTabLabelHomePage) {
|
||||
windowManager.setSize(getIncomingOnlyHomeSize());
|
||||
setResizable(false);
|
||||
} else {
|
||||
windowManager.setSize(getIncomingOnlySettingsSize());
|
||||
setResizable(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -68,14 +79,17 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
onTap: DesktopTabPage.onAddSetting,
|
||||
isClose: false,
|
||||
tail: Offstage(
|
||||
offstage: bind.isIncomingOnly() || bind.isDisableSettings(),
|
||||
child: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
onTap: DesktopTabPage.onAddSetting,
|
||||
isClose: false,
|
||||
),
|
||||
),
|
||||
)));
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(
|
||||
() => DragToResizeArea(
|
||||
|
||||
@@ -53,11 +53,13 @@ class FileManagerPage extends StatefulWidget {
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.isSharedPassword,
|
||||
required this.tabController,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
final DesktopTabController tabController;
|
||||
|
||||
@@ -84,13 +86,14 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
_ffi.start(widget.id,
|
||||
isFileTransfer: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||
if (!Platform.isLinux) {
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
debugPrint("File manager page init success with id ${widget.id}");
|
||||
@@ -103,7 +106,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
model.close().whenComplete(() {
|
||||
_ffi.close();
|
||||
_ffi.dialogManager.dismissAll();
|
||||
if (!Platform.isLinux) {
|
||||
if (!isLinux) {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||
@@ -1295,7 +1298,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
onPointerSignal: (e) {
|
||||
if (e is PointerScrollEvent) {
|
||||
final sc = _breadCrumbScroller;
|
||||
final scale = Platform.isWindows ? 2 : 4;
|
||||
final scale = isWindows ? 2 : 4;
|
||||
sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -45,6 +44,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
password: params['password'],
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: params['forceRelay'],
|
||||
)));
|
||||
@@ -74,6 +74,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
password: args['password'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
)));
|
||||
@@ -90,19 +91,22 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton().paddingOnly(left: 10),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
)),
|
||||
);
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
));
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
);
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
|
||||
@@ -25,19 +25,21 @@ class _PortForward {
|
||||
}
|
||||
|
||||
class PortForwardPage extends StatefulWidget {
|
||||
const PortForwardPage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.tabController,
|
||||
required this.isRDP,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
const PortForwardPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.tabController,
|
||||
required this.isRDP,
|
||||
required this.isSharedPassword,
|
||||
this.forceRelay,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final DesktopTabController tabController;
|
||||
final bool isRDP;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
|
||||
@override
|
||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||
@@ -58,6 +60,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
_ffi.start(widget.id,
|
||||
isPortForward: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
isRdp: widget.isRDP);
|
||||
Get.put(_ffi, tag: 'pf_${widget.id}');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -44,6 +43,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
password: params['password'],
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
isRDP: isRDP,
|
||||
forceRelay: params['forceRelay'],
|
||||
@@ -79,6 +79,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
key: ValueKey(args['id']),
|
||||
id: id,
|
||||
password: args['password'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
isRDP: isRDP,
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
@@ -96,22 +97,31 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: () async {
|
||||
tabController.clear();
|
||||
return true;
|
||||
},
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
)),
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: () async {
|
||||
tabController.clear();
|
||||
return true;
|
||||
},
|
||||
tail: AddButton(),
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
),
|
||||
);
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(
|
||||
context,
|
||||
Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: child),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
);
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(
|
||||
() => SubWindowDragToResizeArea(
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_custom_cursor/cursor_manager.dart'
|
||||
as custom_cursor_manager;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
|
||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
@@ -27,6 +24,9 @@ import '../widgets/remote_toolbar.dart';
|
||||
import '../widgets/kb_layout_type_chooser.dart';
|
||||
import '../widgets/tabbar_widget.dart';
|
||||
|
||||
import 'package:flutter_hbb/native/custom_cursor.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
|
||||
|
||||
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
||||
|
||||
// Used to skip session close if "move to new window" is clicked.
|
||||
@@ -36,15 +36,16 @@ class RemotePage extends StatefulWidget {
|
||||
RemotePage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.sessionId,
|
||||
required this.tabWindowId,
|
||||
required this.display,
|
||||
required this.displays,
|
||||
required this.password,
|
||||
required this.toolbarState,
|
||||
required this.tabController,
|
||||
this.sessionId,
|
||||
this.tabWindowId,
|
||||
this.password,
|
||||
this.display,
|
||||
this.displays,
|
||||
this.tabController,
|
||||
this.switchUuid,
|
||||
this.forceRelay,
|
||||
this.isSharedPassword,
|
||||
}) : super(key: key);
|
||||
|
||||
final String id;
|
||||
@@ -56,8 +57,9 @@ class RemotePage extends StatefulWidget {
|
||||
final ToolbarState toolbarState;
|
||||
final String? switchUuid;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final SimpleWrapper<State<RemotePage>?> _lastState = SimpleWrapper(null);
|
||||
final DesktopTabController tabController;
|
||||
final DesktopTabController? tabController;
|
||||
|
||||
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
|
||||
|
||||
@@ -111,6 +113,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.start(
|
||||
widget.id,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
switchUuid: widget.switchUuid,
|
||||
forceRelay: widget.forceRelay,
|
||||
tabWindowId: widget.tabWindowId,
|
||||
@@ -122,12 +125,12 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
if (!Platform.isLinux) {
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
|
||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
// Session option should be set after models.dart/FFI.start
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
@@ -148,7 +151,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
// }
|
||||
|
||||
_blockableOverlayState.applyFfi(_ffi);
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -157,21 +160,23 @@ class _RemotePageState extends State<RemotePage>
|
||||
// On windows, we use `focus` way to handle keyboard better.
|
||||
// Now on Linux, there's some rdev issues which will break the input.
|
||||
// We disable the `focus` way for non-Windows temporarily.
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
_isWindowBlur = true;
|
||||
// unfocus the primary-focus when the whole window is lost focus,
|
||||
// and let OS to handle events instead.
|
||||
_rawKeyFocusNode.unfocus();
|
||||
}
|
||||
stateGlobal.isFocused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
super.onWindowFocus();
|
||||
// See [onWindowBlur].
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
stateGlobal.isFocused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -179,10 +184,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
super.onWindowRestore();
|
||||
// On windows, we use `onWindowRestore` way to handle window restore from
|
||||
// a minimized state.
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
}
|
||||
@@ -191,7 +196,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
super.onWindowMaximize();
|
||||
if (!Platform.isLinux) {
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
}
|
||||
@@ -199,7 +204,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
super.onWindowMinimize();
|
||||
if (!Platform.isLinux) {
|
||||
if (!isLinux) {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
@@ -225,7 +230,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
if (!isLinux) {
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
await Get.delete<FFI>(tag: widget.id);
|
||||
@@ -263,7 +268,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
debugPrint(
|
||||
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
|
||||
// See [onWindowBlur].
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
if (_isWindowBlur) {
|
||||
imageFocused = false;
|
||||
Future.delayed(Duration.zero, () {
|
||||
@@ -359,7 +364,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
}
|
||||
// See [onWindowBlur].
|
||||
if (!Platform.isWindows) {
|
||||
if (!isWindows) {
|
||||
if (!_rawKeyFocusNode.hasFocus) {
|
||||
_rawKeyFocusNode.requestFocus();
|
||||
}
|
||||
@@ -382,7 +387,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
}
|
||||
// See [onWindowBlur].
|
||||
if (!Platform.isWindows) {
|
||||
if (!isWindows) {
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
}
|
||||
}
|
||||
@@ -429,9 +434,9 @@ class _RemotePageState extends State<RemotePage>
|
||||
Widget getBodyForDesktop(BuildContext context) {
|
||||
var paints = <Widget>[
|
||||
MouseRegion(onEnter: (evt) {
|
||||
bind.hostStopSystemKeyPropagate(stopped: false);
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
|
||||
}, onExit: (evt) {
|
||||
bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
@@ -534,7 +539,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
double getCursorScale() {
|
||||
var c = Provider.of<CanvasModel>(context);
|
||||
var cursorScale = 1.0;
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
// debug win10
|
||||
if (zoomCursor.value && isViewAdaptive()) {
|
||||
cursorScale = s * c.devicePixelRatio;
|
||||
@@ -601,8 +606,8 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
c,
|
||||
s,
|
||||
Offset(
|
||||
Platform.isLinux ? c.x.toInt().toDouble() : c.x,
|
||||
Platform.isLinux ? c.y.toInt().toDouble() : c.y,
|
||||
isLinux ? c.x.toInt().toDouble() : c.x,
|
||||
isLinux ? c.y.toInt().toDouble() : c.y,
|
||||
),
|
||||
c.size,
|
||||
isViewOriginal())
|
||||
@@ -665,43 +670,16 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
);
|
||||
}
|
||||
|
||||
MouseCursor _buildCursorOfCache(
|
||||
CursorModel cursor, double scale, CursorData? cache) {
|
||||
if (cache == null) {
|
||||
return MouseCursor.defer;
|
||||
} else {
|
||||
final key = cache.updateGetKey(scale);
|
||||
if (!cursor.cachedKeys.contains(key)) {
|
||||
debugPrint(
|
||||
"Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
|
||||
// [Safety]
|
||||
// It's ok to call async registerCursor in current synchronous context,
|
||||
// because activating the cursor is also an async call and will always
|
||||
// be executed after this.
|
||||
custom_cursor_manager.CursorManager.instance
|
||||
.registerCursor(custom_cursor_manager.CursorData()
|
||||
..buffer = cache.data!
|
||||
..height = (cache.height * cache.scale).toInt()
|
||||
..width = (cache.width * cache.scale).toInt()
|
||||
..hotX = cache.hotx
|
||||
..hotY = cache.hoty
|
||||
..name = key);
|
||||
cursor.addKey(key);
|
||||
}
|
||||
return FlutterCustomMemoryImageCursor(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
MouseCursor _buildCustomCursor(BuildContext context, double scale) {
|
||||
final cursor = Provider.of<CursorModel>(context);
|
||||
final cache = cursor.cache ?? preDefaultCursor.cache;
|
||||
return _buildCursorOfCache(cursor, scale, cache);
|
||||
return buildCursorOfCache(cursor, scale, cache);
|
||||
}
|
||||
|
||||
MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
|
||||
final cursor = Provider.of<CursorModel>(context);
|
||||
final cache = preForbiddenCursor.cache;
|
||||
return _buildCursorOfCache(cursor, scale, cache);
|
||||
return buildCursorOfCache(cursor, scale, cache);
|
||||
}
|
||||
|
||||
Widget _buildCrossScrollbarFromLayout(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
@@ -8,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
@@ -95,6 +95,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
tabController: tabController,
|
||||
switchUuid: params['switch_uuid'],
|
||||
forceRelay: params['forceRelay'],
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
),
|
||||
));
|
||||
_update_remote_count();
|
||||
@@ -107,106 +108,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
|
||||
dynamic returnValue;
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewRemoteDesktop) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final switchUuid = args['switch_uuid'];
|
||||
final sessionId = args['session_id'];
|
||||
final tabWindowId = args['tab_window_id'];
|
||||
final display = args['display'];
|
||||
final displays = args['displays'];
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
windowOnTop(windowId());
|
||||
tryMoveToScreenAndSetFullscreen(screenRect);
|
||||
if (tabController.length == 0) {
|
||||
// Show the hidden window.
|
||||
if (Platform.isMacOS && stateGlobal.closeOnFullscreen == true) {
|
||||
stateGlobal.setFullscreen(true);
|
||||
}
|
||||
// Reset the state
|
||||
stateGlobal.closeOnFullscreen = null;
|
||||
}
|
||||
ConnectionTypeState.init(id);
|
||||
_toolbarState.setShow(
|
||||
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
||||
tabController.add(TabInfo(
|
||||
key: id,
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
switchUuid: switchUuid,
|
||||
forceRelay: args['forceRelay'],
|
||||
),
|
||||
));
|
||||
} else if (call.method == kWindowDisableGrabKeyboard) {
|
||||
// ???
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventActiveSession) {
|
||||
final jumpOk = tabController.jumpToByKey(call.arguments);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventActiveDisplaySession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final display = args['display'];
|
||||
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventGetRemoteList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => e.key)
|
||||
.toList()
|
||||
.join(',');
|
||||
} else if (call.method == kWindowEventGetSessionIdList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
|
||||
.toList()
|
||||
.join(';');
|
||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||
// Ready to show new window and close old tab.
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final close = args['close'];
|
||||
try {
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == id)
|
||||
.page as RemotePage;
|
||||
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to get cached session data: $e');
|
||||
}
|
||||
if (close && returnValue != null) {
|
||||
closeSessionOnDispose[id] = false;
|
||||
tabController.closeBy(id);
|
||||
}
|
||||
}
|
||||
_update_remote_count();
|
||||
return returnValue;
|
||||
});
|
||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||
if (!_isScreenRectSet) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(
|
||||
@@ -229,104 +131,104 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabWidget = Obx(
|
||||
() => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: MyTheme.color(context).border!,
|
||||
width: stateGlobal.windowBorderWidth.value),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton().paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
final connectionType = ConnectionTypeState.find(key);
|
||||
if (!connectionType.isValid()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
label,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
bool secure =
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
String msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
if (fingerprint.isEmpty) {
|
||||
fingerprint = 'N/A';
|
||||
}
|
||||
if (fingerprint.length > 5 * 8) {
|
||||
var first = fingerprint.substring(0, 39);
|
||||
var second = fingerprint.substring(40);
|
||||
msgFingerprint += '$first\n$second';
|
||||
} else {
|
||||
msgFingerprint += fingerprint;
|
||||
}
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
final connectionType = ConnectionTypeState.find(key);
|
||||
if (!connectionType.isValid()) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
label,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
bool secure =
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
String msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
if (fingerprint.isEmpty) {
|
||||
fingerprint = 'N/A';
|
||||
}
|
||||
if (fingerprint.length > 5 * 8) {
|
||||
var first = fingerprint.substring(0, 39);
|
||||
var second = fingerprint.substring(40);
|
||||
msgFingerprint += '$first\n$second';
|
||||
} else {
|
||||
msgFingerprint += fingerprint;
|
||||
}
|
||||
|
||||
final tab = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Tooltip(
|
||||
message: '$msgConn\n$msgFingerprint',
|
||||
child: SvgPicture.asset(
|
||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
||||
width: themeConf.iconSize,
|
||||
height: themeConf.iconSize,
|
||||
).paddingOnly(right: 5),
|
||||
),
|
||||
label,
|
||||
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
||||
.marginOnly(left: 4),
|
||||
],
|
||||
);
|
||||
final tab = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Tooltip(
|
||||
message: '$msgConn\n$msgFingerprint',
|
||||
child: SvgPicture.asset(
|
||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
||||
width: themeConf.iconSize,
|
||||
height: themeConf.iconSize,
|
||||
).paddingOnly(right: 5),
|
||||
),
|
||||
label,
|
||||
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
||||
.marginOnly(left: 4),
|
||||
],
|
||||
);
|
||||
|
||||
return Listener(
|
||||
onPointerDown: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
return;
|
||||
}
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == key)
|
||||
.page as RemotePage;
|
||||
if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
|
||||
e.buttons == 2) {
|
||||
showRightMenu(
|
||||
(CancelFunc cancelFunc) {
|
||||
return _tabMenuBuilder(key, cancelFunc);
|
||||
},
|
||||
target: e.position,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: tab,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
return Listener(
|
||||
onPointerDown: (e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
return;
|
||||
}
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == key)
|
||||
.page as RemotePage;
|
||||
if (remotePage.ffi.ffiModel.pi.isSet.isTrue && e.buttons == 2) {
|
||||
showRightMenu(
|
||||
(CancelFunc cancelFunc) {
|
||||
return _tabMenuBuilder(key, cancelFunc);
|
||||
},
|
||||
target: e.position,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: tab,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: MyTheme.color(context).border!,
|
||||
width: stateGlobal.windowBorderWidth.value),
|
||||
),
|
||||
child: child,
|
||||
));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(() => SubWindowDragToResizeArea(
|
||||
key: contentKey,
|
||||
@@ -498,4 +400,130 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
_update_remote_count() =>
|
||||
RemoteCountState.find().value = tabController.length;
|
||||
|
||||
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
||||
print(
|
||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
|
||||
dynamic returnValue;
|
||||
// for simplify, just replace connectionId
|
||||
if (call.method == kWindowEventNewRemoteDesktop) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final switchUuid = args['switch_uuid'];
|
||||
final sessionId = args['session_id'];
|
||||
final tabWindowId = args['tab_window_id'];
|
||||
final display = args['display'];
|
||||
final displays = args['displays'];
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
windowOnTop(windowId());
|
||||
tryMoveToScreenAndSetFullscreen(screenRect);
|
||||
if (tabController.length == 0) {
|
||||
// Show the hidden window.
|
||||
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
|
||||
stateGlobal.setFullscreen(true);
|
||||
}
|
||||
// Reset the state
|
||||
stateGlobal.closeOnFullscreen = null;
|
||||
}
|
||||
ConnectionTypeState.init(id);
|
||||
_toolbarState.setShow(
|
||||
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
||||
tabController.add(TabInfo(
|
||||
key: id,
|
||||
label: id,
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () => tabController.closeBy(id),
|
||||
page: RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||
tabWindowId: tabWindowId,
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
toolbarState: _toolbarState,
|
||||
tabController: tabController,
|
||||
switchUuid: switchUuid,
|
||||
forceRelay: args['forceRelay'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
),
|
||||
));
|
||||
} else if (call.method == kWindowDisableGrabKeyboard) {
|
||||
// ???
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
} else if (call.method == kWindowEventActiveSession) {
|
||||
final jumpOk = tabController.jumpToByKey(call.arguments);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventActiveDisplaySession) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final display = args['display'];
|
||||
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
|
||||
if (jumpOk) {
|
||||
windowOnTop(windowId());
|
||||
}
|
||||
return jumpOk;
|
||||
} else if (call.method == kWindowEventGetRemoteList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => e.key)
|
||||
.toList()
|
||||
.join(',');
|
||||
} else if (call.method == kWindowEventGetSessionIdList) {
|
||||
return tabController.state.value.tabs
|
||||
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
|
||||
.toList()
|
||||
.join(';');
|
||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||
// Ready to show new window and close old tab.
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
final close = args['close'];
|
||||
try {
|
||||
final remotePage = tabController.state.value.tabs
|
||||
.firstWhere((tab) => tab.key == id)
|
||||
.page as RemotePage;
|
||||
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to get cached session data: $e');
|
||||
}
|
||||
if (close && returnValue != null) {
|
||||
closeSessionOnDispose[id] = false;
|
||||
tabController.closeBy(id);
|
||||
}
|
||||
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||
final remotePage =
|
||||
tabController.state.value.selectedTabInfo.page as RemotePage;
|
||||
final ffi = remotePage.ffi;
|
||||
final displayRect = ffi.ffiModel.displaysRect();
|
||||
if (displayRect != null) {
|
||||
final wc = WindowController.fromWindowId(windowId());
|
||||
Rect? frame;
|
||||
try {
|
||||
frame = await wc.getFrame();
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
"Failed to get frame of window $windowId, it may be hidden");
|
||||
}
|
||||
if (frame != null) {
|
||||
ffi.cursorModel.moveLocal(0, 0);
|
||||
final coords = RemoteWindowCoords(
|
||||
frame,
|
||||
CanvasCoords.fromCanvasModel(ffi.canvasModel),
|
||||
CursorCoords.fromCursorModel(ffi.cursorModel),
|
||||
displayRect);
|
||||
returnValue = jsonEncode(coords.toJson());
|
||||
}
|
||||
}
|
||||
}
|
||||
_update_remote_count();
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// original cm window in Sciter version.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
@@ -52,7 +52,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
@override
|
||||
void onWindowClose() {
|
||||
Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
|
||||
if (Platform.isMacOS) {
|
||||
if (isMacOS) {
|
||||
RdPlatformChannel.instance.terminate();
|
||||
} else {
|
||||
windowManager.setPreventClose(false);
|
||||
@@ -77,14 +77,20 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
ChangeNotifierProvider.value(value: gFFI.chatModel),
|
||||
],
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, serverModel, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
builder: (context, serverModel, child) {
|
||||
final body = Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: ConnectionManager(),
|
||||
),
|
||||
),
|
||||
);
|
||||
return isLinux
|
||||
? buildVirtualWindowFrame(context, body)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: MyTheme.color(context).border!)),
|
||||
child: body,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -158,7 +164,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
controller: serverModel.tabController,
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
maxLabelWidth: 100,
|
||||
tail: buildScrollJumper(),
|
||||
tail: null, //buildScrollJumper(),
|
||||
selectedTabBackgroundColor:
|
||||
Theme.of(context).hintColor.withOpacity(0),
|
||||
tabBuilder: (key, icon, label, themeConf) {
|
||||
@@ -327,11 +333,7 @@ class _AppIcon extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
width: 30,
|
||||
height: 30,
|
||||
),
|
||||
child: loadIcon(30),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -655,7 +657,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
||||
translate('Enable recording session'),
|
||||
),
|
||||
// only windows support block input
|
||||
if (Platform.isWindows)
|
||||
if (isWindows)
|
||||
buildPermissionIcon(
|
||||
client.blockInput,
|
||||
Icons.block,
|
||||
@@ -706,17 +708,86 @@ class _CmControlPanel extends StatelessWidget {
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !client.inVoiceCall,
|
||||
child: buildButton(
|
||||
context,
|
||||
color: Colors.red,
|
||||
onClick: () => closeVoiceCall(),
|
||||
icon: Icon(
|
||||
Icons.call_end_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Stop voice call",
|
||||
textColor: Colors.white,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: buildButton(context,
|
||||
color: MyTheme.accent,
|
||||
onClick: null, onTapDown: (details) async {
|
||||
final devicesInfo = await AudioInput.getDevicesInfo();
|
||||
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
msgBox(
|
||||
gFFI.sessionId,
|
||||
'custom-nocancel-info',
|
||||
'Prompt',
|
||||
'no_audio_input_device_tip',
|
||||
'',
|
||||
gFFI.dialogManager,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String currentDevice = devicesInfo['current'] as String;
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
final position = RelativeRect.fromLTRB(x, y, x, y);
|
||||
showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: devices
|
||||
.map((d) => PopupMenuItem<String>(
|
||||
value: d,
|
||||
height: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () => AudioInput.setDevice(d),
|
||||
child: IgnorePointer(
|
||||
child: RadioMenuButton(
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) AudioInput.setDevice(v);
|
||||
},
|
||||
child: Container(
|
||||
child: Text(
|
||||
d,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth:
|
||||
kConnectionManagerWindowSizeClosedChat
|
||||
.width -
|
||||
80),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.call_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Audio input",
|
||||
textColor: Colors.white),
|
||||
),
|
||||
Expanded(
|
||||
child: buildButton(
|
||||
context,
|
||||
color: Colors.red,
|
||||
onClick: () => closeVoiceCall(),
|
||||
icon: Icon(
|
||||
Icons.call_end_rounded,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
text: "Stop voice call",
|
||||
textColor: Colors.white,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
@@ -877,12 +948,14 @@ class _CmControlPanel extends StatelessWidget {
|
||||
|
||||
Widget buildButton(BuildContext context,
|
||||
{required Color? color,
|
||||
required Function() onClick,
|
||||
Icon? icon,
|
||||
GestureTapCallback? onClick,
|
||||
Widget? icon,
|
||||
BoxBorder? border,
|
||||
required String text,
|
||||
required Color? textColor,
|
||||
String? tooltip}) {
|
||||
String? tooltip,
|
||||
GestureTapDownCallback? onTapDown}) {
|
||||
assert(!(onClick == null && onTapDown == null));
|
||||
Widget textWidget;
|
||||
if (icon != null) {
|
||||
textWidget = Text(
|
||||
@@ -906,7 +979,16 @@ class _CmControlPanel extends StatelessWidget {
|
||||
color: color, borderRadius: borderRadius, border: border),
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
onTap: () => checkClickTime(client.id, onClick),
|
||||
onTap: () {
|
||||
if (onClick == null) return;
|
||||
checkClickTime(client.id, onClick);
|
||||
},
|
||||
onTapDown: (details) {
|
||||
if (onTapDown == null) return;
|
||||
checkClickTime(client.id, () {
|
||||
onTapDown.call(details);
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
||||
@@ -20,6 +20,7 @@ class DesktopFileTransferScreen extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: FileManagerTabPage(
|
||||
params: params,
|
||||
),
|
||||
|
||||
@@ -17,6 +17,7 @@ class DesktopPortForwardScreen extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: PortForwardTabPage(
|
||||
params: params,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
|
||||
@@ -28,7 +26,7 @@ class DesktopRemoteScreen extends StatelessWidget {
|
||||
child: Scaffold(
|
||||
// Set transparent background for padding the resize area out of the flutter view.
|
||||
// This allows the wallpaper goes through our resize area. (Linux only now).
|
||||
backgroundColor: Platform.isLinux ? Colors.transparent : null,
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: ConnectionTabPage(
|
||||
params: params,
|
||||
),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
@@ -175,9 +174,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
|
||||
return localPlatform;
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
if (isWindows) {
|
||||
localPlatform = kPeerPlatformWindows;
|
||||
} else if (Platform.isLinux) {
|
||||
} else if (isLinux) {
|
||||
localPlatform = kPeerPlatformLinux;
|
||||
}
|
||||
// to-do: web desktop support ?
|
||||
|
||||
@@ -568,6 +568,47 @@ class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// Compatible with MenuEntrySwitch, it uses value instead of getter
|
||||
class MenuEntrySwitchSync<T> extends MenuEntrySwitchBase<T> {
|
||||
final SwitchSetter setter;
|
||||
final RxBool _curOption = false.obs;
|
||||
|
||||
MenuEntrySwitchSync({
|
||||
required SwitchType switchType,
|
||||
required String text,
|
||||
required bool currentValue,
|
||||
required this.setter,
|
||||
Rx<TextStyle>? textStyle,
|
||||
EdgeInsets? padding,
|
||||
dismissOnClicked = false,
|
||||
RxBool? enabled,
|
||||
dismissCallback,
|
||||
}) : super(
|
||||
switchType: switchType,
|
||||
text: text,
|
||||
textStyle: textStyle,
|
||||
padding: padding,
|
||||
dismissOnClicked: dismissOnClicked,
|
||||
enabled: enabled,
|
||||
dismissCallback: dismissCallback,
|
||||
) {
|
||||
_curOption.value = currentValue;
|
||||
}
|
||||
|
||||
@override
|
||||
RxBool get curOption => _curOption;
|
||||
@override
|
||||
setOption(bool? option) async {
|
||||
if (option != null) {
|
||||
await setter(option);
|
||||
// Notice: no ensure with getter, best used on menus that are destroyed on click
|
||||
if (_curOption.value != option) {
|
||||
_curOption.value = option;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef Switch2Getter = RxBool Function();
|
||||
typedef Switch2Setter = Future<void> Function(bool);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
@@ -112,10 +112,10 @@ class _ToolbarTheme {
|
||||
static const double iconRadius = 8;
|
||||
static const double elevation = 3;
|
||||
|
||||
static double dividerSpaceToAction = Platform.isWindows ? 8 : 14;
|
||||
static double dividerSpaceToAction = isWindows ? 8 : 14;
|
||||
|
||||
static double menuBorderRadius = Platform.isWindows ? 5.0 : 7.0;
|
||||
static EdgeInsets menuPadding = Platform.isWindows
|
||||
static double menuBorderRadius = isWindows ? 5.0 : 7.0;
|
||||
static EdgeInsets menuPadding = isWindows
|
||||
? EdgeInsets.fromLTRB(4, 12, 4, 12)
|
||||
: EdgeInsets.fromLTRB(6, 14, 6, 14);
|
||||
static const double menuButtonBorderRadius = 3.0;
|
||||
@@ -492,7 +492,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
||||
}
|
||||
toolbarItems.add(_RecordMenu());
|
||||
if (!isWeb) toolbarItems.add(_RecordMenu());
|
||||
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
|
||||
final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
|
||||
return Column(
|
||||
@@ -637,7 +637,7 @@ class _MonitorMenu extends StatelessWidget {
|
||||
menuStyle: MenuStyle(
|
||||
padding:
|
||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||
menuChildren: [buildMonitorSubmenuWidget()]);
|
||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget()]);
|
||||
}
|
||||
|
||||
Widget buildMultiMonitorMenu() {
|
||||
@@ -759,15 +759,18 @@ class _MonitorMenu extends StatelessWidget {
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < pi.displays.length; i++) {
|
||||
final d = pi.displays[i];
|
||||
final fontSize = (d.width * scale < d.height * scale
|
||||
? d.width * scale
|
||||
: d.height * scale) *
|
||||
double s = d.scale;
|
||||
int dWidth = d.width.toDouble() ~/ s;
|
||||
int dHeight = d.height.toDouble() ~/ s;
|
||||
final fontSize = (dWidth * scale < dHeight * scale
|
||||
? dWidth * scale
|
||||
: dHeight * scale) *
|
||||
0.65;
|
||||
children.add(Positioned(
|
||||
left: (d.x - rect.left) * scale + startX,
|
||||
top: (d.y - rect.top) * scale + startY,
|
||||
width: d.width * scale,
|
||||
height: d.height * scale,
|
||||
width: dWidth * scale,
|
||||
height: dHeight * scale,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
@@ -841,17 +844,17 @@ class _ControlMenu extends StatelessWidget {
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
ffi: ffi,
|
||||
menuChildren: toolbarControls(context, id, ffi).map((e) {
|
||||
if (e.divider) {
|
||||
return Divider();
|
||||
} else {
|
||||
return MenuButton(
|
||||
child: e.child,
|
||||
onPressed: e.onPressed,
|
||||
ffi: ffi,
|
||||
trailingIcon: e.trailingIcon);
|
||||
}
|
||||
}).toList());
|
||||
menuChildrenGetter: () => toolbarControls(context, id, ffi).map((e) {
|
||||
if (e.divider) {
|
||||
return Divider();
|
||||
} else {
|
||||
return MenuButton(
|
||||
child: e.child,
|
||||
onPressed: e.onPressed,
|
||||
ffi: ffi,
|
||||
trailingIcon: e.trailingIcon);
|
||||
}
|
||||
}).toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -938,13 +941,12 @@ class ScreenAdjustor {
|
||||
}
|
||||
|
||||
updateScreen() async {
|
||||
final v = await rustDeskWinManager.call(
|
||||
WindowType.Main, kWindowGetWindowInfo, '');
|
||||
final String valueStr = v.result;
|
||||
if (valueStr.isEmpty) {
|
||||
final String info =
|
||||
isWeb ? screenInfo : await _getScreenInfoDesktop() ?? '';
|
||||
if (info.isEmpty) {
|
||||
_screen = null;
|
||||
} else {
|
||||
final screenMap = jsonDecode(valueStr);
|
||||
final screenMap = jsonDecode(info);
|
||||
_screen = window_size.Screen(
|
||||
Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'],
|
||||
screenMap['frame']['r'], screenMap['frame']['b']),
|
||||
@@ -957,15 +959,23 @@ class ScreenAdjustor {
|
||||
}
|
||||
}
|
||||
|
||||
_getScreenInfoDesktop() async {
|
||||
final v = await rustDeskWinManager.call(
|
||||
WindowType.Main, kWindowGetWindowInfo, '');
|
||||
return v.result;
|
||||
}
|
||||
|
||||
Future<bool> isWindowCanBeAdjusted() async {
|
||||
final viewStyle =
|
||||
await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? '';
|
||||
if (viewStyle != kRemoteViewStyleOriginal) {
|
||||
return false;
|
||||
}
|
||||
final remoteCount = RemoteCountState.find().value;
|
||||
if (remoteCount != 1) {
|
||||
return false;
|
||||
if (!isWeb) {
|
||||
final remoteCount = RemoteCountState.find().value;
|
||||
if (remoteCount != 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (_screen == null) {
|
||||
return false;
|
||||
@@ -1036,54 +1046,63 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_screenAdjustor.updateScreen();
|
||||
|
||||
final menuChildren = <Widget>[
|
||||
_screenAdjustor.adjustWindow(context),
|
||||
viewStyle(),
|
||||
scrollStyle(),
|
||||
imageQuality(),
|
||||
codec(),
|
||||
_ResolutionsMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
screenAdjustor: _screenAdjustor,
|
||||
),
|
||||
// We may add this feature if it is needed and we have an EV certificate.
|
||||
// _VirtualDisplayMenu(
|
||||
// id: widget.id,
|
||||
// ffi: widget.ffi,
|
||||
// ),
|
||||
Divider(),
|
||||
toggles(),
|
||||
];
|
||||
// privacy mode
|
||||
if (ffiModel.keyboard && pi.features.privacyMode) {
|
||||
final privacyModeState = PrivacyModeState.find(id);
|
||||
final privacyModeList =
|
||||
toolbarPrivacyMode(privacyModeState, context, id, ffi);
|
||||
if (privacyModeList.length == 1) {
|
||||
menuChildren.add(CkbMenuButton(
|
||||
value: privacyModeList[0].value,
|
||||
onChanged: privacyModeList[0].onChanged,
|
||||
child: privacyModeList[0].child,
|
||||
ffi: ffi));
|
||||
} else if (privacyModeList.length > 1) {
|
||||
menuChildren.addAll([
|
||||
Divider(),
|
||||
_SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
child: Text(translate('Privacy mode')),
|
||||
menuChildren: privacyModeList
|
||||
.map((e) => CkbMenuButton(
|
||||
value: e.value,
|
||||
onChanged: e.onChanged,
|
||||
child: e.child,
|
||||
ffi: ffi))
|
||||
.toList()),
|
||||
]);
|
||||
menuChildrenGetter() {
|
||||
final menuChildren = <Widget>[
|
||||
_screenAdjustor.adjustWindow(context),
|
||||
viewStyle(),
|
||||
scrollStyle(),
|
||||
imageQuality(),
|
||||
codec(),
|
||||
_ResolutionsMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
screenAdjustor: _screenAdjustor,
|
||||
),
|
||||
if (pi.isRustDeskIdd)
|
||||
_RustDeskVirtualDisplayMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
if (pi.isAmyuniIdd)
|
||||
_AmyuniVirtualDisplayMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
Divider(),
|
||||
cursorToggles(),
|
||||
Divider(),
|
||||
toggles(),
|
||||
];
|
||||
// privacy mode
|
||||
if (ffiModel.keyboard && pi.features.privacyMode) {
|
||||
final privacyModeState = PrivacyModeState.find(id);
|
||||
final privacyModeList =
|
||||
toolbarPrivacyMode(privacyModeState, context, id, ffi);
|
||||
if (privacyModeList.length == 1) {
|
||||
menuChildren.add(CkbMenuButton(
|
||||
value: privacyModeList[0].value,
|
||||
onChanged: privacyModeList[0].onChanged,
|
||||
child: privacyModeList[0].child,
|
||||
ffi: ffi));
|
||||
} else if (privacyModeList.length > 1) {
|
||||
menuChildren.addAll([
|
||||
Divider(),
|
||||
_SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
child: Text(translate('Privacy mode')),
|
||||
menuChildren: privacyModeList
|
||||
.map((e) => CkbMenuButton(
|
||||
value: e.value,
|
||||
onChanged: e.onChanged,
|
||||
child: e.child,
|
||||
ffi: ffi))
|
||||
.toList()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
menuChildren.add(widget.pluginItem);
|
||||
return menuChildren;
|
||||
}
|
||||
menuChildren.add(widget.pluginItem);
|
||||
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Display Settings',
|
||||
@@ -1091,7 +1110,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildren: menuChildren,
|
||||
menuChildrenGetter: menuChildrenGetter,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1195,6 +1214,23 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
});
|
||||
}
|
||||
|
||||
cursorToggles() {
|
||||
return futureBuilder(
|
||||
future: toolbarCursor(context, id, ffi),
|
||||
hasData: (data) {
|
||||
final v = data as List<TToggleMenu>;
|
||||
if (v.isEmpty) return Offstage();
|
||||
return Column(
|
||||
children: v
|
||||
.map((e) => CkbMenuButton(
|
||||
value: e.value,
|
||||
onChanged: e.onChanged,
|
||||
child: e.child,
|
||||
ffi: ffi))
|
||||
.toList());
|
||||
});
|
||||
}
|
||||
|
||||
toggles() {
|
||||
return futureBuilder(
|
||||
future: toolbarDisplayToggle(context, id, ffi),
|
||||
@@ -1244,7 +1280,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
FFI get ffi => widget.ffi;
|
||||
PeerInfo get pi => widget.ffi.ffiModel.pi;
|
||||
FfiModel get ffiModel => widget.ffi.ffiModel;
|
||||
Rect? get rect => ffiModel.rect;
|
||||
Rect? get rect => scaledRect();
|
||||
List<Resolution> get resolutions => pi.resolutions;
|
||||
bool get isWayland => bind.mainCurrentIsWayland();
|
||||
|
||||
@@ -1254,6 +1290,20 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
_getLocalResolutionWayland();
|
||||
}
|
||||
|
||||
Rect? scaledRect() {
|
||||
final scale = pi.scaleOfDisplay(pi.currentDisplay);
|
||||
final rect = ffiModel.rect;
|
||||
if (rect == null) {
|
||||
return null;
|
||||
}
|
||||
return Rect.fromLTWH(
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.width / scale,
|
||||
rect.height / scale,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isVirtualDisplay = ffiModel.isVirtualDisplayResolution;
|
||||
@@ -1287,7 +1337,8 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
if (lastGroupValue == _kCustomResolutionValue) {
|
||||
_groupValue = _kCustomResolutionValue;
|
||||
} else {
|
||||
_groupValue = '${rect?.width.toInt()}x${rect?.height.toInt()}';
|
||||
_groupValue =
|
||||
'${(rect?.width ?? 0).toInt()}x${(rect?.height ?? 0).toInt()}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1321,6 +1372,14 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
final display = json.decode(mainDisplay);
|
||||
if (display['w'] != null && display['h'] != null) {
|
||||
_localResolution = Resolution(display['w'], display['h']);
|
||||
if (isWeb) {
|
||||
if (display['scaleFactor'] != null) {
|
||||
_localResolution = Resolution(
|
||||
(display['w'] / display['scaleFactor']).toInt(),
|
||||
(display['h'] / display['scaleFactor']).toInt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to decode $mainDisplay, $e');
|
||||
@@ -1386,6 +1445,11 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
if (display == null) {
|
||||
return Offstage();
|
||||
}
|
||||
if (!resolutions.any((e) =>
|
||||
e.width == display.originalWidth &&
|
||||
e.height == display.originalHeight)) {
|
||||
return Offstage();
|
||||
}
|
||||
return Offstage(
|
||||
offstage: !showOriginalBtn,
|
||||
child: MenuButton(
|
||||
@@ -1500,21 +1564,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _VirtualDisplayMenu extends StatefulWidget {
|
||||
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
|
||||
_VirtualDisplayMenu({
|
||||
_RustDeskVirtualDisplayMenu({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState();
|
||||
State<_RustDeskVirtualDisplayMenu> createState() =>
|
||||
_RustDeskVirtualDisplayMenuState();
|
||||
}
|
||||
|
||||
class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
||||
class _RustDeskVirtualDisplayMenuState
|
||||
extends State<_RustDeskVirtualDisplayMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -1529,7 +1595,7 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays;
|
||||
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||
|
||||
final children = <Widget>[];
|
||||
@@ -1571,6 +1637,82 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
||||
}
|
||||
}
|
||||
|
||||
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
|
||||
_AmyuniVirtualDisplayMenu({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.ffi,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_AmyuniVirtualDisplayMenu> createState() =>
|
||||
_AmiyuniVirtualDisplayMenuState();
|
||||
}
|
||||
|
||||
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||
return Offstage();
|
||||
}
|
||||
if (!widget.ffi.ffiModel.pi.isInstalled) {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||
|
||||
final children = <Widget>[
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||
? null
|
||||
: () => bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId, index: 0, on: false),
|
||||
child: Icon(Icons.remove),
|
||||
),
|
||||
Text(count.toString()),
|
||||
TextButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 4
|
||||
? null
|
||||
: () => bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId, index: 0, on: true),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
)),
|
||||
Divider(),
|
||||
Obx(() => MenuButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false);
|
||||
},
|
||||
ffi: widget.ffi,
|
||||
child: Text(translate('Plug out all')),
|
||||
)),
|
||||
];
|
||||
|
||||
return _SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
menuChildren: children,
|
||||
child: Text(translate("Virtual display")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyboardMenu extends StatelessWidget {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
@@ -1586,19 +1728,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
var ffiModel = Provider.of<FfiModel>(context);
|
||||
if (!ffiModel.keyboard) return Offstage();
|
||||
// If use flutter to grab keys, we can only use one mode.
|
||||
// Map mode and Legacy mode, at least one of them is supported.
|
||||
String? modeOnly;
|
||||
if (isInputSourceFlutter) {
|
||||
if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
||||
modeOnly = kKeyMapMode;
|
||||
} else if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: ffi.sessionId, mode: kKeyLegacyMode)) {
|
||||
modeOnly = kKeyLegacyMode;
|
||||
}
|
||||
}
|
||||
final toolbarToggles = toolbarKeyboardToggles(ffi)
|
||||
toolbarToggles() => toolbarKeyboardToggles(ffi)
|
||||
.map((e) => CkbMenuButton(
|
||||
value: e.value, onChanged: e.onChanged, child: e.child, ffi: ffi))
|
||||
.toList();
|
||||
@@ -1608,18 +1738,18 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
ffi: ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildren: [
|
||||
keyboardMode(modeOnly),
|
||||
localKeyboardType(),
|
||||
inputSource(),
|
||||
Divider(),
|
||||
viewMode(),
|
||||
Divider(),
|
||||
...toolbarToggles,
|
||||
]);
|
||||
menuChildrenGetter: () => [
|
||||
keyboardMode(),
|
||||
localKeyboardType(),
|
||||
inputSource(),
|
||||
Divider(),
|
||||
viewMode(),
|
||||
Divider(),
|
||||
...toolbarToggles(),
|
||||
]);
|
||||
}
|
||||
|
||||
keyboardMode(String? modeOnly) {
|
||||
keyboardMode() {
|
||||
return futureBuilder(future: () async {
|
||||
return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ??
|
||||
kKeyLegacyMode;
|
||||
@@ -1639,6 +1769,19 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
await ffi.inputModel.updateKeyboardMode();
|
||||
}
|
||||
|
||||
// If use flutter to grab keys, we can only use one mode.
|
||||
// Map mode and Legacy mode, at least one of them is supported.
|
||||
String? modeOnly;
|
||||
if (isInputSourceFlutter) {
|
||||
if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
||||
modeOnly = kKeyMapMode;
|
||||
} else if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: ffi.sessionId, mode: kKeyLegacyMode)) {
|
||||
modeOnly = kKeyLegacyMode;
|
||||
}
|
||||
}
|
||||
|
||||
for (InputModeMenu mode in modes) {
|
||||
if (modeOnly != null && mode.key != modeOnly) {
|
||||
continue;
|
||||
@@ -1732,7 +1875,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
? (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId, value: 'view-only');
|
||||
sessionId: ffi.sessionId, value: kOptionViewOnly);
|
||||
ffiModel.setViewOnly(id, value);
|
||||
}
|
||||
: null,
|
||||
@@ -1767,7 +1910,7 @@ class _ChatMenuState extends State<_ChatMenu> {
|
||||
ffi: widget.ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildren: [textChat(), voiceCall()]);
|
||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||
}
|
||||
|
||||
textChat() {
|
||||
@@ -1811,34 +1954,71 @@ class _VoiceCallMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
menuChildrenGetter() {
|
||||
final audioInput =
|
||||
AudioInput(builder: (devices, currentDevice, setDevice) {
|
||||
return Column(
|
||||
children: devices
|
||||
.map((d) => RdoMenuButton<String>(
|
||||
child: Container(
|
||||
child: Text(
|
||||
d,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: 250),
|
||||
),
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) setDevice(v);
|
||||
},
|
||||
ffi: ffi,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
return [
|
||||
audioInput,
|
||||
Divider(),
|
||||
MenuButton(
|
||||
child: Text(translate('End call')),
|
||||
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
ffi: ffi,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return Obx(
|
||||
() {
|
||||
final String tooltip;
|
||||
final String icon;
|
||||
switch (ffi.chatModel.voiceCallStatus.value) {
|
||||
case VoiceCallStatus.waitingForResponse:
|
||||
tooltip = "Waiting";
|
||||
icon = "assets/call_wait.svg";
|
||||
break;
|
||||
return buildCallWaiting(context);
|
||||
case VoiceCallStatus.connected:
|
||||
tooltip = "Disconnect";
|
||||
icon = "assets/call_end.svg";
|
||||
break;
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Voice call',
|
||||
svg: 'assets/voice_call.svg',
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
menuChildrenGetter: menuChildrenGetter,
|
||||
ffi: ffi,
|
||||
);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
return _IconMenuButton(
|
||||
assetName: icon,
|
||||
tooltip: tooltip,
|
||||
onPressed: () =>
|
||||
bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
color: _ToolbarTheme.redColor,
|
||||
hoverColor: _ToolbarTheme.hoverRedColor);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildCallWaiting(BuildContext context) {
|
||||
return _IconMenuButton(
|
||||
assetName: "assets/call_wait.svg",
|
||||
tooltip: "Waiting",
|
||||
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||
color: _ToolbarTheme.redColor,
|
||||
hoverColor: _ToolbarTheme.hoverRedColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
class _RecordMenu extends StatelessWidget {
|
||||
const _RecordMenu({Key? key}) : super(key: key);
|
||||
|
||||
@@ -1971,9 +2151,9 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
final Widget? icon;
|
||||
final Color color;
|
||||
final Color hoverColor;
|
||||
final List<Widget> menuChildren;
|
||||
final List<Widget> Function() menuChildrenGetter;
|
||||
final MenuStyle? menuStyle;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
final double? width;
|
||||
|
||||
_IconSubmenuButton({
|
||||
@@ -1983,8 +2163,8 @@ class _IconSubmenuButton extends StatefulWidget {
|
||||
required this.tooltip,
|
||||
required this.color,
|
||||
required this.hoverColor,
|
||||
required this.menuChildren,
|
||||
required this.ffi,
|
||||
required this.menuChildrenGetter,
|
||||
this.ffi,
|
||||
this.menuStyle,
|
||||
this.width,
|
||||
}) : super(key: key);
|
||||
@@ -2027,7 +2207,8 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
|
||||
color: hover ? widget.hoverColor : widget.color,
|
||||
),
|
||||
child: icon))),
|
||||
menuChildren: widget.menuChildren
|
||||
menuChildren: widget
|
||||
.menuChildrenGetter()
|
||||
.map((e) => _buildPointerTrackWidget(e, widget.ffi))
|
||||
.toList()));
|
||||
return MenuBar(children: [
|
||||
@@ -2065,13 +2246,13 @@ class MenuButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final Widget? trailingIcon;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
MenuButton(
|
||||
{Key? key,
|
||||
this.onPressed,
|
||||
this.trailingIcon,
|
||||
required this.child,
|
||||
required this.ffi})
|
||||
this.ffi})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -2080,7 +2261,9 @@ class MenuButton extends StatelessWidget {
|
||||
key: key,
|
||||
onPressed: onPressed != null
|
||||
? () {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onPressed?.call();
|
||||
}
|
||||
: null,
|
||||
@@ -2093,13 +2276,13 @@ class CkbMenuButton extends StatelessWidget {
|
||||
final bool? value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
const CkbMenuButton(
|
||||
{Key? key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
required this.child,
|
||||
required this.ffi})
|
||||
this.ffi})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -2110,7 +2293,9 @@ class CkbMenuButton extends StatelessWidget {
|
||||
child: child,
|
||||
onChanged: onChanged != null
|
||||
? (bool? value) {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onChanged?.call(value);
|
||||
}
|
||||
: null,
|
||||
@@ -2123,13 +2308,13 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
final T? groupValue;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final Widget? child;
|
||||
final FFI ffi;
|
||||
final FFI? ffi;
|
||||
const RdoMenuButton({
|
||||
Key? key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.child,
|
||||
required this.ffi,
|
||||
this.ffi,
|
||||
this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -2141,7 +2326,9 @@ class RdoMenuButton<T> extends StatelessWidget {
|
||||
child: child,
|
||||
onChanged: onChanged != null
|
||||
? (T? value) {
|
||||
_menuDismissCallback(ffi);
|
||||
if (ffi != null) {
|
||||
_menuDismissCallback(ffi!);
|
||||
}
|
||||
onChanged?.call(value);
|
||||
}
|
||||
: null,
|
||||
@@ -2328,10 +2515,11 @@ class InputModeMenu {
|
||||
|
||||
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
|
||||
|
||||
Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
|
||||
Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
|
||||
return Listener(
|
||||
onPointerHover: (PointerHoverEvent e) =>
|
||||
ffi.inputModel.lastMousePos = e.position,
|
||||
onPointerHover: (PointerHoverEvent e) => {
|
||||
if (ffi != null) {ffi.inputModel.lastMousePos = e.position}
|
||||
},
|
||||
child: MouseRegion(
|
||||
child: child,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
@@ -13,11 +12,11 @@ import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
|
||||
import 'package:scroll_pos/scroll_pos.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
import '../../utils/multi_window_manager.dart';
|
||||
|
||||
@@ -166,7 +165,8 @@ class DesktopTabController {
|
||||
}));
|
||||
}
|
||||
});
|
||||
if (callOnSelected) {
|
||||
if ((isDesktop && (bind.isIncomingOnly() || bind.isOutgoingOnly())) ||
|
||||
callOnSelected) {
|
||||
if (state.value.tabs.length > index) {
|
||||
final key = state.value.tabs[index].key;
|
||||
onSelected?.call(key);
|
||||
@@ -257,6 +257,8 @@ class DesktopTab extends StatelessWidget {
|
||||
late final DesktopTabType tabType;
|
||||
late final bool isMainWindow;
|
||||
|
||||
final RxList<String> invisibleTabKeys = RxList.empty();
|
||||
|
||||
DesktopTab({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
@@ -375,7 +377,8 @@ class DesktopTab extends StatelessWidget {
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
// custom double tap handler
|
||||
onTap: showMaximize
|
||||
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
|
||||
showMaximize
|
||||
? () {
|
||||
final current = DateTime.now().millisecondsSinceEpoch;
|
||||
final elapsed = current - _lastClickTime;
|
||||
@@ -391,20 +394,17 @@ class DesktopTab extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !Platform.isMacOS,
|
||||
offstage: !isMacOS,
|
||||
child: const SizedBox(
|
||||
width: 78,
|
||||
)),
|
||||
Offstage(
|
||||
offstage: kUseCompatibleUiMode || Platform.isMacOS,
|
||||
offstage: kUseCompatibleUiMode || isMacOS,
|
||||
child: Row(children: [
|
||||
Offstage(
|
||||
offstage: !showLogo,
|
||||
child: SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
width: 16,
|
||||
height: 16,
|
||||
)),
|
||||
offstage: !showLogo,
|
||||
child: loadIcon(16),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !showTitle,
|
||||
child: const Text(
|
||||
@@ -433,6 +433,7 @@ class DesktopTab extends StatelessWidget {
|
||||
},
|
||||
child: _ListView(
|
||||
controller: controller,
|
||||
invisibleTabKeys: invisibleTabKeys,
|
||||
tabBuilder: tabBuilder,
|
||||
tabMenuBuilder: tabMenuBuilder,
|
||||
labelGetter: labelGetter,
|
||||
@@ -451,12 +452,14 @@ class DesktopTab extends StatelessWidget {
|
||||
tabType: tabType,
|
||||
state: state,
|
||||
tabController: controller,
|
||||
invisibleTabKeys: invisibleTabKeys,
|
||||
tail: tail,
|
||||
showMinimize: showMinimize,
|
||||
showMaximize: showMaximize,
|
||||
showClose: showClose,
|
||||
onClose: onWindowCloseButton,
|
||||
)
|
||||
labelGetter: labelGetter,
|
||||
).paddingOnly(left: 10)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -474,17 +477,22 @@ class WindowActionPanel extends StatefulWidget {
|
||||
final Widget? tail;
|
||||
final Future<bool> Function()? onClose;
|
||||
|
||||
final RxList<String> invisibleTabKeys;
|
||||
final LabelGetter? labelGetter;
|
||||
|
||||
const WindowActionPanel(
|
||||
{Key? key,
|
||||
required this.isMainWindow,
|
||||
required this.tabType,
|
||||
required this.state,
|
||||
required this.tabController,
|
||||
required this.invisibleTabKeys,
|
||||
this.tail,
|
||||
this.showMinimize = true,
|
||||
this.showMaximize = true,
|
||||
this.showClose = true,
|
||||
this.onClose})
|
||||
this.onClose,
|
||||
this.labelGetter})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -540,6 +548,16 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
stateGlobal.isFocused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
stateGlobal.isFocused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
stateGlobal.setMinimized(true);
|
||||
@@ -586,8 +604,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
mainWindowClose() async => await windowManager.hide();
|
||||
notMainWindowClose(WindowController controller) async {
|
||||
if (widget.tabController.length != 0) {
|
||||
debugPrint("close not emtpy multiwindow from taskbar");
|
||||
if (Platform.isWindows) {
|
||||
debugPrint("close not empty multiwindow from taskbar");
|
||||
if (isWindows) {
|
||||
await controller.show();
|
||||
await controller.focus();
|
||||
final res = await widget.onClose?.call() ?? true;
|
||||
@@ -622,7 +640,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
|
||||
}
|
||||
// macOS specific workaround, the window is not hiding when in fullscreen.
|
||||
if (Platform.isMacOS && await windowManager.isFullScreen()) {
|
||||
if (isMacOS && await windowManager.isFullScreen()) {
|
||||
stateGlobal.closeOnFullscreen ??= true;
|
||||
await windowManager.setFullScreen(false);
|
||||
await macOSWindowClose(
|
||||
@@ -636,7 +654,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
} else {
|
||||
// it's safe to hide the subwindow
|
||||
final controller = WindowController.fromWindowId(kWindowId!);
|
||||
if (Platform.isMacOS) {
|
||||
if (isMacOS) {
|
||||
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
|
||||
// use ??= to make sure the value is set on first call.
|
||||
|
||||
@@ -661,18 +679,41 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
super.onWindowClose();
|
||||
}
|
||||
|
||||
bool showTabDowndown() {
|
||||
return widget.tabController.state.value.tabs.length > 1 &&
|
||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||
widget.tabController.tabType == DesktopTabType.fileTransfer ||
|
||||
widget.tabController.tabType == DesktopTabType.portForward ||
|
||||
widget.tabController.tabType == DesktopTabType.cm);
|
||||
}
|
||||
|
||||
List<String> existingInvisibleTab() {
|
||||
return widget.invisibleTabKeys
|
||||
.where((key) =>
|
||||
widget.tabController.state.value.tabs.any((tab) => tab.key == key))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Obx(() => Offstage(
|
||||
offstage:
|
||||
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
|
||||
child: _TabDropDownButton(
|
||||
controller: widget.tabController,
|
||||
labelGetter: widget.labelGetter,
|
||||
tabkeys: existingInvisibleTab()),
|
||||
)),
|
||||
Offstage(offstage: widget.tail == null, child: widget.tail),
|
||||
Offstage(
|
||||
offstage: kUseCompatibleUiMode,
|
||||
child: Row(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !widget.showMinimize || Platform.isMacOS,
|
||||
offstage: !widget.showMinimize || isMacOS,
|
||||
child: ActionIcon(
|
||||
message: 'Minimize',
|
||||
icon: IconFont.min,
|
||||
@@ -686,7 +727,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
isClose: false,
|
||||
)),
|
||||
Offstage(
|
||||
offstage: !widget.showMaximize || Platform.isMacOS,
|
||||
offstage: !widget.showMaximize || isMacOS,
|
||||
child: Obx(() => ActionIcon(
|
||||
message: stateGlobal.isMaximized.isTrue
|
||||
? 'Restore'
|
||||
@@ -694,11 +735,13 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
icon: stateGlobal.isMaximized.isTrue
|
||||
? IconFont.restore
|
||||
: IconFont.max,
|
||||
onTap: _toggleMaximize,
|
||||
onTap: bind.isIncomingOnly() && isInHomePage()
|
||||
? null
|
||||
: _toggleMaximize,
|
||||
isClose: false,
|
||||
))),
|
||||
Offstage(
|
||||
offstage: !widget.showClose || Platform.isMacOS,
|
||||
offstage: !widget.showClose || isMacOS,
|
||||
child: ActionIcon(
|
||||
message: 'Close',
|
||||
icon: IconFont.close,
|
||||
@@ -815,6 +858,7 @@ Future<bool> closeConfirmDialog() async {
|
||||
|
||||
class _ListView extends StatelessWidget {
|
||||
final DesktopTabController controller;
|
||||
final RxList<String> invisibleTabKeys;
|
||||
|
||||
final TabBuilder? tabBuilder;
|
||||
final TabMenuBuilder? tabMenuBuilder;
|
||||
@@ -826,8 +870,9 @@ class _ListView extends StatelessWidget {
|
||||
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
|
||||
const _ListView({
|
||||
_ListView({
|
||||
required this.controller,
|
||||
required this.invisibleTabKeys,
|
||||
this.tabBuilder,
|
||||
this.tabMenuBuilder,
|
||||
this.labelGetter,
|
||||
@@ -847,6 +892,19 @@ class _ListView extends StatelessWidget {
|
||||
controller.tabType == DesktopTabType.install;
|
||||
}
|
||||
|
||||
onVisibilityChanged(VisibilityInfo info) {
|
||||
final key = (info.key as ValueKey).value;
|
||||
if (info.visibleFraction < 0.75) {
|
||||
if (!invisibleTabKeys.contains(key)) {
|
||||
invisibleTabKeys.add(key);
|
||||
}
|
||||
invisibleTabKeys.removeWhere((key) =>
|
||||
controller.state.value.tabs.where((e) => e.key == key).isEmpty);
|
||||
} else {
|
||||
invisibleTabKeys.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => ListView(
|
||||
@@ -859,35 +917,41 @@ class _ListView extends StatelessWidget {
|
||||
: state.value.tabs.asMap().entries.map((e) {
|
||||
final index = e.key;
|
||||
final tab = e.value;
|
||||
return _Tab(
|
||||
final label = labelGetter == null
|
||||
? Rx<String>(tab.label)
|
||||
: labelGetter!(tab.label);
|
||||
return VisibilityDetector(
|
||||
key: ValueKey(tab.key),
|
||||
index: index,
|
||||
tabInfoKey: tab.key,
|
||||
label: labelGetter == null
|
||||
? Rx<String>(tab.label)
|
||||
: labelGetter!(tab.label),
|
||||
selectedIcon: tab.selectedIcon,
|
||||
unselectedIcon: tab.unselectedIcon,
|
||||
closable: tab.closable,
|
||||
selected: state.value.selected,
|
||||
onClose: () {
|
||||
if (tab.onTabCloseButton != null) {
|
||||
tab.onTabCloseButton!();
|
||||
} else {
|
||||
controller.remove(index);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
controller.jumpTo(index);
|
||||
tab.onTap?.call();
|
||||
},
|
||||
tabBuilder: tabBuilder,
|
||||
tabMenuBuilder: tabMenuBuilder,
|
||||
maxLabelWidth: maxLabelWidth,
|
||||
selectedTabBackgroundColor: selectedTabBackgroundColor ??
|
||||
MyTheme.tabbar(context).selectedTabBackgroundColor,
|
||||
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
|
||||
selectedBorderColor: selectedBorderColor,
|
||||
onVisibilityChanged: onVisibilityChanged,
|
||||
child: _Tab(
|
||||
key: ValueKey(tab.key),
|
||||
index: index,
|
||||
tabInfoKey: tab.key,
|
||||
label: label,
|
||||
tabType: controller.tabType,
|
||||
selectedIcon: tab.selectedIcon,
|
||||
unselectedIcon: tab.unselectedIcon,
|
||||
closable: tab.closable,
|
||||
selected: state.value.selected,
|
||||
onClose: () {
|
||||
if (tab.onTabCloseButton != null) {
|
||||
tab.onTabCloseButton!();
|
||||
} else {
|
||||
controller.remove(index);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
controller.jumpTo(index);
|
||||
tab.onTap?.call();
|
||||
},
|
||||
tabBuilder: tabBuilder,
|
||||
tabMenuBuilder: tabMenuBuilder,
|
||||
maxLabelWidth: maxLabelWidth,
|
||||
selectedTabBackgroundColor: selectedTabBackgroundColor ??
|
||||
MyTheme.tabbar(context).selectedTabBackgroundColor,
|
||||
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
|
||||
selectedBorderColor: selectedBorderColor,
|
||||
),
|
||||
);
|
||||
}).toList()));
|
||||
}
|
||||
@@ -897,6 +961,7 @@ class _Tab extends StatefulWidget {
|
||||
final int index;
|
||||
final String tabInfoKey;
|
||||
final Rx<String> label;
|
||||
final DesktopTabType tabType;
|
||||
final IconData? selectedIcon;
|
||||
final IconData? unselectedIcon;
|
||||
final bool closable;
|
||||
@@ -915,6 +980,7 @@ class _Tab extends StatefulWidget {
|
||||
required this.index,
|
||||
required this.tabInfoKey,
|
||||
required this.label,
|
||||
required this.tabType,
|
||||
this.selectedIcon,
|
||||
this.unselectedIcon,
|
||||
this.tabBuilder,
|
||||
@@ -954,7 +1020,9 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
|
||||
child: Tooltip(
|
||||
message: translate(widget.label.value),
|
||||
message: widget.tabType == DesktopTabType.main
|
||||
? ''
|
||||
: translate(widget.label.value),
|
||||
child: Text(
|
||||
translate(widget.label.value),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -1110,7 +1178,8 @@ class _CloseButton extends StatelessWidget {
|
||||
class ActionIcon extends StatefulWidget {
|
||||
final String? message;
|
||||
final IconData icon;
|
||||
final Function() onTap;
|
||||
final GestureTapCallback? onTap;
|
||||
final GestureTapDownCallback? onTapDown;
|
||||
final bool isClose;
|
||||
final double iconSize;
|
||||
final double boxSize;
|
||||
@@ -1119,7 +1188,8 @@ class ActionIcon extends StatefulWidget {
|
||||
{Key? key,
|
||||
this.message,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
this.onTap,
|
||||
this.onTapDown,
|
||||
this.isClose = false,
|
||||
this.iconSize = _kActionIconSize,
|
||||
this.boxSize = _kTabBarHeight - 1})
|
||||
@@ -1143,24 +1213,31 @@ class _ActionIconState extends State<ActionIcon> {
|
||||
return Tooltip(
|
||||
message: widget.message != null ? translate(widget.message!) : "",
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: Obx(
|
||||
() => InkWell(
|
||||
hoverColor: widget.isClose
|
||||
? const Color.fromARGB(255, 196, 43, 28)
|
||||
: MyTheme.tabbar(context).hoverColor,
|
||||
onHover: (value) => hover.value = value,
|
||||
onTap: widget.onTap,
|
||||
child: SizedBox(
|
||||
height: widget.boxSize,
|
||||
width: widget.boxSize,
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: hover.value && widget.isClose
|
||||
? Colors.white
|
||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||
size: widget.iconSize,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
hoverColor: widget.isClose
|
||||
? const Color.fromARGB(255, 196, 43, 28)
|
||||
: MyTheme.tabbar(context).hoverColor,
|
||||
onHover: (value) => hover.value = value,
|
||||
onTap: widget.onTap,
|
||||
onTapDown: widget.onTapDown,
|
||||
child: SizedBox(
|
||||
height: widget.boxSize,
|
||||
width: widget.boxSize,
|
||||
child: widget.onTap == null
|
||||
? Icon(
|
||||
widget.icon,
|
||||
color: Colors.grey,
|
||||
size: widget.iconSize,
|
||||
)
|
||||
: Obx(
|
||||
() => Icon(
|
||||
widget.icon,
|
||||
color: hover.value && widget.isClose
|
||||
? Colors.white
|
||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||
size: widget.iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1183,6 +1260,103 @@ class AddButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _TabDropDownButton extends StatefulWidget {
|
||||
final DesktopTabController controller;
|
||||
final List<String> tabkeys;
|
||||
final LabelGetter? labelGetter;
|
||||
|
||||
const _TabDropDownButton(
|
||||
{required this.controller, required this.tabkeys, this.labelGetter});
|
||||
|
||||
@override
|
||||
State<_TabDropDownButton> createState() => _TabDropDownButtonState();
|
||||
}
|
||||
|
||||
class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
||||
var position = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<String> sortedKeys = widget.controller.state.value.tabs
|
||||
.where((e) => widget.tabkeys.contains(e.key))
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
return ActionIcon(
|
||||
onTapDown: (details) {
|
||||
final x = details.globalPosition.dx;
|
||||
final y = details.globalPosition.dy;
|
||||
position = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
icon: Icons.arrow_drop_down,
|
||||
onTap: () {
|
||||
showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
items: sortedKeys.map((e) {
|
||||
var label = e;
|
||||
final tabInfo = widget.controller.state.value.tabs
|
||||
.firstWhereOrNull((element) => element.key == e);
|
||||
if (tabInfo != null) {
|
||||
label = tabInfo.label;
|
||||
}
|
||||
if (widget.labelGetter != null) {
|
||||
label = widget.labelGetter!(e).value;
|
||||
}
|
||||
var index = widget.controller.state.value.tabs
|
||||
.indexWhere((t) => t.key == e);
|
||||
label = '${index + 1}. $label';
|
||||
final menuHover = false.obs;
|
||||
final btnHover = false.obs;
|
||||
return PopupMenuItem<String>(
|
||||
value: e,
|
||||
height: 32,
|
||||
onTap: () {
|
||||
widget.controller.jumpToByKey(e);
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
onHover: (event) => setState(() => menuHover.value = true),
|
||||
onExit: (event) => setState(() => menuHover.value = false),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(child: Text(label)),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !(tabInfo?.onTabCloseButton != null &&
|
||||
menuHover.value),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
tabInfo?.onTabCloseButton?.call();
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onHover: (event) =>
|
||||
setState(() => btnHover.value = true),
|
||||
onExit: (event) =>
|
||||
setState(() => btnHover.value = false),
|
||||
child: Icon(Icons.close,
|
||||
color:
|
||||
btnHover.value ? Colors.red : null))),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||
final Color? selectedTabIconColor;
|
||||
final Color? unSelectedTabIconColor;
|
||||
|
||||
@@ -14,21 +14,21 @@ import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/plugin/handlers.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
// import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'common.dart';
|
||||
import 'consts.dart';
|
||||
import 'mobile/pages/home_page.dart';
|
||||
import 'mobile/pages/server_page.dart';
|
||||
import 'models/platform_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/plugin/handlers.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
|
||||
|
||||
/// Basic window and launch properties.
|
||||
int? kWindowId;
|
||||
WindowType? kWindowType;
|
||||
@@ -36,6 +36,7 @@ late List<String> kBootArgs;
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
debugPrint("launch args: $args");
|
||||
kBootArgs = List.from(args);
|
||||
|
||||
@@ -47,7 +48,7 @@ Future<void> main(List<String> args) async {
|
||||
if (args.isNotEmpty && args.first == 'multi_window') {
|
||||
kWindowId = int.parse(args[1]);
|
||||
stateGlobal.setWindowId(kWindowId!);
|
||||
if (!Platform.isMacOS) {
|
||||
if (!isMacOS) {
|
||||
WindowController.fromWindowId(kWindowId!).showTitleBar(false);
|
||||
}
|
||||
final argument = args[2].isEmpty
|
||||
@@ -58,14 +59,12 @@ Future<void> main(List<String> args) async {
|
||||
// Because stateGlobal.windowId is a global value.
|
||||
argument['windowId'] = kWindowId;
|
||||
kWindowType = type.windowType;
|
||||
final windowName = getWindowName();
|
||||
switch (kWindowType) {
|
||||
case WindowType.RemoteDesktop:
|
||||
desktopType = DesktopType.remote;
|
||||
runMultiWindow(
|
||||
argument,
|
||||
kAppTypeDesktopRemote,
|
||||
windowName,
|
||||
);
|
||||
break;
|
||||
case WindowType.FileTransfer:
|
||||
@@ -73,7 +72,6 @@ Future<void> main(List<String> args) async {
|
||||
runMultiWindow(
|
||||
argument,
|
||||
kAppTypeDesktopFileTransfer,
|
||||
windowName,
|
||||
);
|
||||
break;
|
||||
case WindowType.PortForward:
|
||||
@@ -81,7 +79,6 @@ Future<void> main(List<String> args) async {
|
||||
runMultiWindow(
|
||||
argument,
|
||||
kAppTypeDesktopPortForward,
|
||||
windowName,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -128,6 +125,7 @@ void runMainApp(bool startService) async {
|
||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
runApp(App());
|
||||
|
||||
// Set window option.
|
||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions();
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
@@ -146,25 +144,27 @@ void runMainApp(bool startService) async {
|
||||
}
|
||||
windowManager.setOpacity(1);
|
||||
windowManager.setTitle(getWindowName());
|
||||
// Do not use `windowManager.setResizable()` here.
|
||||
setResizable(!bind.isIncomingOnly());
|
||||
});
|
||||
}
|
||||
|
||||
void runMobileApp() async {
|
||||
await initEnv(kAppTypeMain);
|
||||
if (isAndroid) androidChannelInit();
|
||||
platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
runApp(App());
|
||||
await initUniLinks();
|
||||
if (!isWeb) await initUniLinks();
|
||||
}
|
||||
|
||||
void runMultiWindow(
|
||||
Map<String, dynamic> argument,
|
||||
String appType,
|
||||
String title,
|
||||
) async {
|
||||
await initEnv(appType);
|
||||
final title = getWindowName();
|
||||
// set prevent close to true, we handle close event manually
|
||||
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
||||
late Widget widget;
|
||||
@@ -239,7 +239,7 @@ void runConnectionManagerScreen() async {
|
||||
} else {
|
||||
await showCmWindow(isStartup: true);
|
||||
}
|
||||
windowManager.setResizable(false);
|
||||
setResizable(false);
|
||||
// Start the uni links handler and redirect links to Native, not for Flutter.
|
||||
listenUniLinks(handleByFlutter: false);
|
||||
}
|
||||
@@ -249,7 +249,7 @@ bool _isCmReadyToShow = false;
|
||||
showCmWindow({bool isStartup = false}) async {
|
||||
if (isStartup) {
|
||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
||||
size: kConnectionManagerWindowSizeClosedChat);
|
||||
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||
bind.mainHideDocker();
|
||||
await Future.wait([
|
||||
@@ -343,7 +343,7 @@ void runInstallPage() async {
|
||||
}
|
||||
|
||||
WindowOptions getHiddenTitleBarWindowOptions(
|
||||
{Size? size, bool center = false}) {
|
||||
{Size? size, bool center = false, bool? alwaysOnTop}) {
|
||||
var defaultTitleBarStyle = TitleBarStyle.hidden;
|
||||
// we do not hide titlebar on win7 because of the frame overflow.
|
||||
if (kUseCompatibleUiMode) {
|
||||
@@ -355,6 +355,7 @@ WindowOptions getHiddenTitleBarWindowOptions(
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: defaultTitleBarStyle,
|
||||
alwaysOnTop: alwaysOnTop,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -441,6 +442,9 @@ class _AppState extends State<App> {
|
||||
if (isDesktop && desktopType == DesktopType.main) {
|
||||
child = keyListenerBuilder(context, child);
|
||||
}
|
||||
if (isLinux) {
|
||||
child = buildVirtualWindowFrame(context, child);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
|
||||
@@ -53,7 +53,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_uniLinksSubscription = listenUniLinks();
|
||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
@@ -84,7 +84,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
_buildUpdateUI(),
|
||||
if (!bind.isCustomClient()) _buildUpdateUI(),
|
||||
_buildRemoteIDTextField(),
|
||||
])),
|
||||
SliverFillRemaining(
|
||||
@@ -166,6 +166,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
platform: '',
|
||||
tags: [],
|
||||
hash: '',
|
||||
password: '',
|
||||
forceAlwaysRelay: false,
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
@@ -12,8 +11,12 @@ import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
FileManagerPage({Key? key, required this.id}) : super(key: key);
|
||||
FileManagerPage(
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
@@ -68,7 +71,10 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
gFFI.start(widget.id, isFileTransfer: true);
|
||||
gFFI.start(widget.id,
|
||||
isFileTransfer: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/chat_page.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import 'connection_page.dart';
|
||||
|
||||
abstract class PageShape extends Widget {
|
||||
@@ -25,8 +26,9 @@ class HomePageState extends State<HomePage> {
|
||||
var _selectedIndex = 0;
|
||||
int get selectedIndex => _selectedIndex;
|
||||
final List<PageShape> _pages = [];
|
||||
int _chatPageTabIndex = -1;
|
||||
bool get isChatPageCurrentTab => isAndroid
|
||||
? _selectedIndex == 1
|
||||
? _selectedIndex == _chatPageTabIndex
|
||||
: false; // change this when ios have chat page
|
||||
|
||||
void refreshPages() {
|
||||
@@ -43,8 +45,9 @@ class HomePageState extends State<HomePage> {
|
||||
|
||||
void initPages() {
|
||||
_pages.clear();
|
||||
_pages.add(ConnectionPage());
|
||||
if (isAndroid) {
|
||||
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
|
||||
if (isAndroid && !bind.isOutgoingOnly()) {
|
||||
_chatPageTabIndex = _pages.length;
|
||||
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
||||
}
|
||||
_pages.add(SettingsPage());
|
||||
@@ -141,7 +144,7 @@ class HomePageState extends State<HomePage> {
|
||||
],
|
||||
);
|
||||
}
|
||||
return Text("RustDesk");
|
||||
return Text(bind.mainGetAppNameSync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +157,7 @@ class WebHomePage extends StatelessWidget {
|
||||
// backgroundColor: MyTheme.grayBg,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text("RustDesk${isWeb ? " (Beta) " : ""}"),
|
||||
title: Text(bind.mainGetAppNameSync()),
|
||||
actions: connectionPage.appBarActions,
|
||||
),
|
||||
body: connectionPage,
|
||||
|
||||
@@ -25,9 +25,12 @@ import '../widgets/dialog.dart';
|
||||
final initText = '1' * 1024;
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
RemotePage({Key? key, required this.id}) : super(key: key);
|
||||
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
: super(key: key);
|
||||
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
|
||||
@override
|
||||
State<RemotePage> createState() => _RemotePageState();
|
||||
@@ -54,15 +57,21 @@ class _RemotePageState extends State<RemotePage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
gFFI.start(widget.id);
|
||||
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
gFFI.start(
|
||||
widget.id,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
WakelockPlus.enable();
|
||||
if (!isWeb) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
_physicalFocusNode.requestFocus();
|
||||
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
gFFI.inputModel.listenToMouse(true);
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
keyboardSubscription =
|
||||
@@ -88,7 +97,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
await WakelockPlus.disable();
|
||||
if (!isWeb) {
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
await keyboardSubscription.cancel();
|
||||
removeSharedStates(widget.id);
|
||||
}
|
||||
@@ -212,7 +223,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
||||
// show now, and sleep a while to requestFocus to
|
||||
// make sure edit ready, so that keyboard wont show/hide/show/hide happen
|
||||
// make sure edit ready, so that keyboard won't show/hide/show/hide happen
|
||||
setState(() => _showEdit = true);
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||
@@ -288,25 +299,26 @@ class _RemotePageState extends State<RemotePage> {
|
||||
: Offstage(),
|
||||
],
|
||||
)),
|
||||
body: getRawPointerAndKeyBody(Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
return Container(
|
||||
body: Obx(
|
||||
() => getRawPointerAndKeyBody(Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: isWebDesktop
|
||||
? getBodyForDesktopWithListener(keyboard)
|
||||
: SafeArea(child:
|
||||
OrientationBuilder(builder: (ctx, orientation) {
|
||||
if (_currentOrientation != orientation) {
|
||||
Timer(const Duration(milliseconds: 200), () {
|
||||
gFFI.dialogManager
|
||||
.resetMobileActionsOverlay(ffi: gFFI);
|
||||
_currentOrientation = orientation;
|
||||
gFFI.canvasModel.updateViewStyle();
|
||||
});
|
||||
}
|
||||
return Obx(
|
||||
() => Container(
|
||||
: SafeArea(
|
||||
child:
|
||||
OrientationBuilder(builder: (ctx, orientation) {
|
||||
if (_currentOrientation != orientation) {
|
||||
Timer(const Duration(milliseconds: 200), () {
|
||||
gFFI.dialogManager
|
||||
.resetMobileActionsOverlay(ffi: gFFI);
|
||||
_currentOrientation = orientation;
|
||||
gFFI.canvasModel.updateViewStyle();
|
||||
});
|
||||
}
|
||||
return Container(
|
||||
color: MyTheme.canvasColor,
|
||||
child: inputModel.isPhysicalMouse.value
|
||||
? getBodyForMobile()
|
||||
@@ -314,12 +326,14 @@ class _RemotePageState extends State<RemotePage> {
|
||||
child: getBodyForMobile(),
|
||||
ffi: gFFI,
|
||||
),
|
||||
),
|
||||
);
|
||||
})));
|
||||
})
|
||||
],
|
||||
))),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
)),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -822,6 +836,7 @@ void showOptions(
|
||||
List<TRadioMenu<String>> imageQualityRadios =
|
||||
await toolbarImageQuality(context, id, gFFI);
|
||||
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
|
||||
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
|
||||
List<TToggleMenu> displayToggles =
|
||||
await toolbarDisplayToggle(context, id, gFFI);
|
||||
|
||||
@@ -862,8 +877,23 @@ void showOptions(
|
||||
})),
|
||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||
];
|
||||
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
||||
final cursorTogglesList = cursorToggles
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => Obx(() => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxCursorToggleValues[e.key].value,
|
||||
onChanged: (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||
},
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
|
||||
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
|
||||
final toggles = displayToggles
|
||||
final displayTogglesList = displayToggles
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => Obx(() => CheckboxListTile(
|
||||
@@ -876,6 +906,11 @@ void showOptions(
|
||||
},
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
final toggles = [
|
||||
...cursorTogglesList,
|
||||
if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border),
|
||||
...displayTogglesList,
|
||||
];
|
||||
|
||||
Widget privacyModeWidget = Offstage();
|
||||
if (privacyModeList.length > 1) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../consts.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
|
||||
class ScanPage extends StatefulWidget {
|
||||
@@ -61,7 +61,7 @@ class _ScanPageState extends State<ScanPage> {
|
||||
var reader = QRCodeReader();
|
||||
try {
|
||||
var result = reader.decode(bitmap);
|
||||
if (result.text.startsWith(kUniLinksPrefix)) {
|
||||
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
||||
handleUriLink(uriString: result.text);
|
||||
} else {
|
||||
showServerSettingFromQr(result.text);
|
||||
|
||||
@@ -42,25 +42,21 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
enabled: gFFI.serverModel.connectStatus > 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
value: "changeID",
|
||||
child: Text(translate("Change ID")),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: 'AcceptSessionsViaPassword',
|
||||
child: listTile(
|
||||
'Accept sessions via password', approveMode == 'password'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: 'AcceptSessionsViaClick',
|
||||
child:
|
||||
listTile('Accept sessions via click', approveMode == 'click'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: "AcceptSessionsViaBoth",
|
||||
child: listTile("Accept sessions via both",
|
||||
approveMode != 'password' && approveMode != 'click'),
|
||||
@@ -69,35 +65,30 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
if (showPasswordOption &&
|
||||
verificationMethod != kUseTemporaryPassword)
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
value: "setPermanentPassword",
|
||||
child: Text(translate("Set permanent password")),
|
||||
),
|
||||
if (showPasswordOption &&
|
||||
verificationMethod != kUsePermanentPassword)
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
value: "setTemporaryPasswordLength",
|
||||
child: Text(translate("One-time password length")),
|
||||
),
|
||||
if (showPasswordOption) const PopupMenuDivider(),
|
||||
if (showPasswordOption)
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: kUseTemporaryPassword,
|
||||
child: listTile('Use one-time password',
|
||||
verificationMethod == kUseTemporaryPassword),
|
||||
),
|
||||
if (showPasswordOption)
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: kUsePermanentPassword,
|
||||
child: listTile('Use permanent password',
|
||||
verificationMethod == kUsePermanentPassword),
|
||||
),
|
||||
if (showPasswordOption)
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: kUseBothPasswords,
|
||||
child: listTile(
|
||||
'Use both passwords',
|
||||
@@ -167,6 +158,7 @@ class _ServerPageState extends State<ServerPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
buildPresetPasswordWarning(),
|
||||
gFFI.serverModel.isStart
|
||||
? ServerInfo()
|
||||
: ServiceNotRunningNotification(),
|
||||
@@ -235,7 +227,7 @@ class ScamWarningDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
int _countdown = 12;
|
||||
int _countdown = bind.isCustomClient() ? 0 : 12;
|
||||
bool show_warning = false;
|
||||
late Timer _timer;
|
||||
late ServerModel _serverModel;
|
||||
@@ -392,7 +384,7 @@ class ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.blueAccent,
|
||||
backgroundColor: Colors.blueAccent,
|
||||
),
|
||||
child: Text(
|
||||
translate("Decline"),
|
||||
|
||||
@@ -27,7 +27,7 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
||||
final icon = Icon(Icons.settings);
|
||||
|
||||
@override
|
||||
final appBarActions = [ScanButton()];
|
||||
final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()];
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsState();
|
||||
@@ -218,6 +218,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
final outgoingOnly = bind.isOutgoingOnly();
|
||||
final customClientSection = CustomSettingsSection(
|
||||
child: Column(
|
||||
children: [
|
||||
if (bind.isCustomClient())
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: loadPowered(context),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: loadLogo(),
|
||||
)
|
||||
],
|
||||
));
|
||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
SettingsTile.switchTile(
|
||||
@@ -448,33 +463,37 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||
}));
|
||||
|
||||
return SettingsList(
|
||||
final disabledSettings = bind.isDisableSettings();
|
||||
final settings = SettingsList(
|
||||
sections: [
|
||||
SettingsSection(
|
||||
title: Text(translate('Account')),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate('Login')
|
||||
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
||||
leading: Icon(Icons.person),
|
||||
onPressed: (context) {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
customClientSection,
|
||||
if (!bind.isDisableAccount())
|
||||
SettingsSection(
|
||||
title: Text(translate('Account')),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate('Login')
|
||||
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
||||
leading: Icon(Icons.person),
|
||||
onPressed: (context) {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||
SettingsTile(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}),
|
||||
if (!disabledSettings)
|
||||
SettingsTile(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}),
|
||||
SettingsTile(
|
||||
title: Text(translate('Language')),
|
||||
leading: Icon(Icons.translate),
|
||||
@@ -494,7 +513,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
},
|
||||
)
|
||||
]),
|
||||
if (isAndroid)
|
||||
if (isAndroid && !outgoingOnly)
|
||||
SettingsSection(
|
||||
title: Text(translate("Recording")),
|
||||
tiles: [
|
||||
@@ -506,7 +525,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
builder: (ctx, data) => Offstage(
|
||||
offstage: !data.hasData,
|
||||
child: Text("${translate("Directory")}: ${data.data}")),
|
||||
future: bind.mainDefaultVideoSaveDirectory()),
|
||||
future: bind.mainVideoSaveDirectory(root: false)),
|
||||
initialValue: _autoRecordIncomingSession,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
@@ -523,13 +542,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isAndroid)
|
||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
||||
SettingsSection(
|
||||
title: Text(translate("Share Screen")),
|
||||
tiles: shareScreenTiles,
|
||||
),
|
||||
defaultDisplaySection(),
|
||||
if (isAndroid)
|
||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
||||
SettingsSection(
|
||||
title: Text(translate("Enhancements")),
|
||||
tiles: enhancementsTiles,
|
||||
@@ -578,6 +597,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
),
|
||||
],
|
||||
);
|
||||
return settings;
|
||||
}
|
||||
|
||||
Future<bool> canStartOnBoot() async {
|
||||
|
||||
@@ -283,12 +283,3 @@ void setPrivacyModeDialog(
|
||||
);
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
Future<String?> validateAsync(String value) async {
|
||||
value = value.trim();
|
||||
if (value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final res = await bind.mainTestIfValidServer(server: value);
|
||||
return res.isEmpty ? null : res;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import './platform_model.dart';
|
||||
|
||||
final useTextureRender =
|
||||
bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender();
|
||||
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
||||
|
||||
// Feature flutter_texture_render need to be enabled if feature vram is enabled.
|
||||
final useTextureRender = !isWeb &&
|
||||
(bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender());
|
||||
|
||||
class _PixelbufferTexture {
|
||||
int _textureKey = -1;
|
||||
@@ -36,7 +40,7 @@ class _PixelbufferTexture {
|
||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||
debugPrint(
|
||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id");
|
||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -98,13 +102,15 @@ class _GpuTexture {
|
||||
}
|
||||
}
|
||||
|
||||
destroy(FFI ffi) async {
|
||||
destroy(bool unregisterTexture, FFI ffi) async {
|
||||
// must stop texture render, render unregistered texture cause crash
|
||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||
_destroying = true;
|
||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
if (unregisterTexture) {
|
||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
}
|
||||
await gpuTextureRenderer.unregisterTexture(_textureId);
|
||||
_textureId = -1;
|
||||
_destroying = false;
|
||||
@@ -149,40 +155,36 @@ class TextureModel {
|
||||
TextureModel(this.parent);
|
||||
|
||||
setTextureType({required int display, required bool gpuTexture}) {
|
||||
debugPrint("setTextureType: display:$display, isGpuTexture:$gpuTexture");
|
||||
var texture = _control[display];
|
||||
if (texture == null) {
|
||||
texture = _Control();
|
||||
_control[display] = texture;
|
||||
debugPrint("setTextureType: display=$display, isGpuTexture=$gpuTexture");
|
||||
ensureControl(display);
|
||||
_control[display]?.setTextureType(gpuTexture: gpuTexture);
|
||||
// For versions that do not support multiple displays, the display parameter is always 0, need set type of current display
|
||||
final ffi = parent.target;
|
||||
if (ffi == null) return;
|
||||
if (!ffi.ffiModel.pi.isSupportMultiDisplay) {
|
||||
final currentDisplay = CurrentDisplayState.find(ffi.id).value;
|
||||
if (currentDisplay != display) {
|
||||
debugPrint(
|
||||
"setTextureType: currentDisplay=$currentDisplay, isGpuTexture=$gpuTexture");
|
||||
ensureControl(currentDisplay);
|
||||
_control[currentDisplay]?.setTextureType(gpuTexture: gpuTexture);
|
||||
}
|
||||
}
|
||||
texture.setTextureType(gpuTexture: gpuTexture);
|
||||
}
|
||||
|
||||
setRgbaTextureId({required int display, required int id}) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
ctl = _Control();
|
||||
_control[display] = ctl;
|
||||
}
|
||||
ctl.setRgbaTextureId(id);
|
||||
ensureControl(display);
|
||||
_control[display]?.setRgbaTextureId(id);
|
||||
}
|
||||
|
||||
setGpuTextureId({required int display, required int id}) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
ctl = _Control();
|
||||
_control[display] = ctl;
|
||||
}
|
||||
ctl.setGpuTextureId(id);
|
||||
ensureControl(display);
|
||||
_control[display]?.setGpuTextureId(id);
|
||||
}
|
||||
|
||||
RxInt getTextureId(int display) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
ctl = _Control();
|
||||
_control[display] = ctl;
|
||||
}
|
||||
return ctl.textureID;
|
||||
ensureControl(display);
|
||||
return _control[display]!.textureID;
|
||||
}
|
||||
|
||||
updateCurrentDisplay(int curDisplay) {
|
||||
@@ -208,7 +210,7 @@ class TextureModel {
|
||||
_pixelbufferRenderTextures.remove(idx);
|
||||
}
|
||||
if (_gpuRenderTextures.containsKey(idx)) {
|
||||
_gpuRenderTextures[idx]!.destroy(ffi);
|
||||
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
||||
_gpuRenderTextures.remove(idx);
|
||||
}
|
||||
}
|
||||
@@ -235,7 +237,15 @@ class TextureModel {
|
||||
await texture.destroy(closeSession, ffi);
|
||||
}
|
||||
for (final texture in _gpuRenderTextures.values) {
|
||||
await texture.destroy(ffi);
|
||||
await texture.destroy(closeSession, ffi);
|
||||
}
|
||||
}
|
||||
|
||||
ensureControl(int display) {
|
||||
var ctl = _control[display];
|
||||
if (ctl == null) {
|
||||
ctl = _Control();
|
||||
_control[display] = ctl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -292,7 +291,7 @@ class FileController {
|
||||
name: isLocal ? "local_show_hidden" : "remote_show_hidden"))
|
||||
.isNotEmpty;
|
||||
options.value.isWindows = isLocal
|
||||
? Platform.isWindows
|
||||
? isWindows
|
||||
: rootState.target?.ffiModel.pi.platform == kPeerPlatformWindows;
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../utils/http_service.dart' as http;
|
||||
|
||||
class GroupModel {
|
||||
final RxBool groupLoading = false.obs;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user