mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-29 16:11:01 +03:00
Compare commits
484 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a0fd55af7 | ||
|
|
da70cbcdda | ||
|
|
921b64e1e0 | ||
|
|
c0de0aa108 | ||
|
|
715d475f49 | ||
|
|
e3f09b3ec6 | ||
|
|
0a5fafb84f | ||
|
|
4e084c5ee0 | ||
|
|
d1fe617670 | ||
|
|
7744bdbbe0 | ||
|
|
e1329c8157 | ||
|
|
f31e60af5b | ||
|
|
ed18e3c786 | ||
|
|
579e0fac36 | ||
|
|
92752765ba | ||
|
|
071f51cf6f | ||
|
|
dde3cce120 | ||
|
|
bb1b9858d5 | ||
|
|
a31c27be73 | ||
|
|
85ae3916cb | ||
|
|
cc9b7e64eb | ||
|
|
5e22a49e49 | ||
|
|
07cf1b4db5 | ||
|
|
f6ab5cdcb2 | ||
|
|
b477aded0b | ||
|
|
65318efd67 | ||
|
|
dbd195a46e | ||
|
|
0651ad492f | ||
|
|
a3c5adb1f4 | ||
|
|
a771abcdc2 | ||
|
|
b8b3a089f3 | ||
|
|
8f00067266 | ||
|
|
83bf067d18 | ||
|
|
1729ee337f | ||
|
|
57834840b8 | ||
|
|
99d7b62d79 | ||
|
|
6625aca994 | ||
|
|
ce56be6507 | ||
|
|
fd69b14623 | ||
|
|
7521bbe15f | ||
|
|
3c6ddd7403 | ||
|
|
6820e2f4c7 | ||
|
|
77f3ebaf1a | ||
|
|
e7e244d4f2 | ||
|
|
f4c40d733e | ||
|
|
049c334db3 | ||
|
|
6197832317 | ||
|
|
2fd53f9825 | ||
|
|
ae16b8975b | ||
|
|
171177c76f | ||
|
|
ade1d8c0c7 | ||
|
|
9a194f0850 | ||
|
|
76d5a8b205 | ||
|
|
bc6ce6c7ee | ||
|
|
025cdfa25b | ||
|
|
2f432e941d | ||
|
|
96edca8f74 | ||
|
|
51b250435d | ||
|
|
5a2121501d | ||
|
|
877b3e2ce5 | ||
|
|
421ddc0016 | ||
|
|
2662abc5a3 | ||
|
|
b3e1c8a907 | ||
|
|
2266fde26f | ||
|
|
e58e75eea9 | ||
|
|
eafebdba21 | ||
|
|
7bf5e69444 | ||
|
|
cb0dc46d08 | ||
|
|
9b8209b61b | ||
|
|
b6ba9978e3 | ||
|
|
0d1d7a9b87 | ||
|
|
508dd5b383 | ||
|
|
31a1b7a80b | ||
|
|
ba43424781 | ||
|
|
f899b2a962 | ||
|
|
2dd3d8c11e | ||
|
|
6eea425280 | ||
|
|
5e7d4fd2d6 | ||
|
|
d9fba50606 | ||
|
|
b6035fbbdf | ||
|
|
e67b694f06 | ||
|
|
2333ee2c07 | ||
|
|
61ccc2152e | ||
|
|
f6aca4ca8e | ||
|
|
1707987a7b | ||
|
|
85604dee79 | ||
|
|
a12969be30 | ||
|
|
9f91eada89 | ||
|
|
cb6a6aa42a | ||
|
|
4fec8abad4 | ||
|
|
35571dc8d7 | ||
|
|
a103b83647 | ||
|
|
e1e4bf599b | ||
|
|
cba8aaa410 | ||
|
|
8ced4ddaa2 | ||
|
|
15404ecab4 | ||
|
|
97772f9ac5 | ||
|
|
764fbe2c9d | ||
|
|
e03344d85b | ||
|
|
0e98a51775 | ||
|
|
0a1d3c4afb | ||
|
|
fd9b5f3c57 | ||
|
|
73f6afd4c0 | ||
|
|
50dd2b3aad | ||
|
|
c0e9445602 | ||
|
|
541d9c6b86 | ||
|
|
8c91e5c5ca | ||
|
|
19d1605d8c | ||
|
|
0faf82f109 | ||
|
|
ee5314de20 | ||
|
|
7e8d3bd2ac | ||
|
|
9750e1409c | ||
|
|
67d4e061fb | ||
|
|
f67f2be0cb | ||
|
|
aa42bf548e | ||
|
|
d679e8fa7d | ||
|
|
1357ef5d6f | ||
|
|
30a5d1e0e1 | ||
|
|
9f0985c842 | ||
|
|
be06c0d738 | ||
|
|
f0f50f0f03 | ||
|
|
1850d32f49 | ||
|
|
3999d498be | ||
|
|
bbdce8d57b | ||
|
|
baf70da2fe | ||
|
|
b967d496cc | ||
|
|
2aef79688b | ||
|
|
0451a1c45f | ||
|
|
f7e9057a39 | ||
|
|
d73e0e1e5a | ||
|
|
39dbd89287 | ||
|
|
c04f460bbd | ||
|
|
79a1f888d6 | ||
|
|
57d1b1ecc4 | ||
|
|
8a4a2b5732 | ||
|
|
51a60a7eed | ||
|
|
b26acde450 | ||
|
|
2de81045ea | ||
|
|
a72a8906b0 | ||
|
|
614086a216 | ||
|
|
2ffc2ad85b | ||
|
|
eef091d4e8 | ||
|
|
97f26f880b | ||
|
|
22c6f5e589 | ||
|
|
b828768fa9 | ||
|
|
31e7b6acf1 | ||
|
|
14b505130b | ||
|
|
22f3425ace | ||
|
|
4723d6a830 | ||
|
|
c4f3c0f133 | ||
|
|
de375c91bb | ||
|
|
d3454f07d3 | ||
|
|
cf8ef2533a | ||
|
|
6ad662260e | ||
|
|
2b54a553c7 | ||
|
|
85ded0a3e5 | ||
|
|
5c16a8302e | ||
|
|
04c175c62e | ||
|
|
2be05608d8 | ||
|
|
c3c99ba107 | ||
|
|
a81d6468cc | ||
|
|
48464835f5 | ||
|
|
edc5d86ee7 | ||
|
|
e9c8ba5393 | ||
|
|
a72bc0fb28 | ||
|
|
5a8c8cbf7c | ||
|
|
b68d7a3054 | ||
|
|
9e931a6f04 | ||
|
|
f0587796e2 | ||
|
|
875ac28ab5 | ||
|
|
6821bef5e5 | ||
|
|
930561f431 | ||
|
|
bc672b3367 | ||
|
|
e283d33f28 | ||
|
|
901505e8be | ||
|
|
a4565bf0da | ||
|
|
092e4089c7 | ||
|
|
188f85b042 | ||
|
|
72c96f22b6 | ||
|
|
0143eaf601 | ||
|
|
09466680d3 | ||
|
|
eec879a801 | ||
|
|
3f11d9cdb6 | ||
|
|
8512c2b2b0 | ||
|
|
e2a7e38a39 | ||
|
|
3a0ece1447 | ||
|
|
d0a54a6cc6 | ||
|
|
bed214bd37 | ||
|
|
5f31211db3 | ||
|
|
29b8875c1c | ||
|
|
3367c541b2 | ||
|
|
30afe4f779 | ||
|
|
d18e95703e | ||
|
|
9adc083def | ||
|
|
0dc664474a | ||
|
|
5e8fe239fa | ||
|
|
7a3100a87c | ||
|
|
8a1acedae5 | ||
|
|
f5bcc17636 | ||
|
|
883c630206 | ||
|
|
a95a6ab733 | ||
|
|
46605fab1b | ||
|
|
9d26fec631 | ||
|
|
294a6ce9bc | ||
|
|
183ea47ba4 | ||
|
|
06e04143a8 | ||
|
|
a532b36e28 | ||
|
|
c873b69662 | ||
|
|
b30f84623b | ||
|
|
888e993534 | ||
|
|
1d59a7fe5f | ||
|
|
2c027cdcf5 | ||
|
|
fe513dd967 | ||
|
|
d652b99d5b | ||
|
|
c2716c2509 | ||
|
|
821f7245b0 | ||
|
|
0ea88ce6ff | ||
|
|
21f41e98a0 | ||
|
|
282ea02ebf | ||
|
|
170200fa49 | ||
|
|
d8cee6507d | ||
|
|
2391b18046 | ||
|
|
b5a7165015 | ||
|
|
ef4d84657b | ||
|
|
011647511c | ||
|
|
e2d217a138 | ||
|
|
f07936a911 | ||
|
|
0bb4d43e9e | ||
|
|
6f74080a2d | ||
|
|
8a370e640a | ||
|
|
d007408061 | ||
|
|
02572e9032 | ||
|
|
af66d2a73b | ||
|
|
eb5ab4d7d9 | ||
|
|
c02b4f994a | ||
|
|
d093fdc256 | ||
|
|
2af799f46e | ||
|
|
3c7e24c605 | ||
|
|
7d961d895b | ||
|
|
53dbc2fa6f | ||
|
|
024220e58a | ||
|
|
8621b93436 | ||
|
|
ac88121c4a | ||
|
|
90df80ed78 | ||
|
|
d4f3a87276 | ||
|
|
48efdcf1f0 | ||
|
|
0511cdbb21 | ||
|
|
8747b9847f | ||
|
|
92d0fe1c3f | ||
|
|
a9015bcf70 | ||
|
|
f8f2686267 | ||
|
|
c2bd1b8965 | ||
|
|
4eeee5b7ee | ||
|
|
dfc224ec01 | ||
|
|
86ff768241 | ||
|
|
94addb162b | ||
|
|
bea65f8739 | ||
|
|
92f570831d | ||
|
|
9349210a87 | ||
|
|
95f4274eca | ||
|
|
a6febb2816 | ||
|
|
e294dafe7c | ||
|
|
d00582e929 | ||
|
|
6d2e985593 | ||
|
|
182e8c4ac0 | ||
|
|
40019b80f6 | ||
|
|
2f40b9dc04 | ||
|
|
8602b036bd | ||
|
|
51db8e706d | ||
|
|
a0dc38f749 | ||
|
|
625b610cfd | ||
|
|
62a8349739 | ||
|
|
0ab500c27c | ||
|
|
285e974d1a | ||
|
|
e71d86c124 | ||
|
|
14343e89d4 | ||
|
|
3f2dfa521c | ||
|
|
cd73368cb9 | ||
|
|
84b5cd70ed | ||
|
|
01672bc697 | ||
|
|
763174657b | ||
|
|
15fa80fb26 | ||
|
|
d537e2563d | ||
|
|
1719e478e3 | ||
|
|
1f129e6ef3 | ||
|
|
25d0ced8ba | ||
|
|
2116fec20b | ||
|
|
1252f45506 | ||
|
|
1f4c62e480 | ||
|
|
bd334769fa | ||
|
|
750368af7b | ||
|
|
2fb35c3596 | ||
|
|
5114a9d369 | ||
|
|
4b6ba7938f | ||
|
|
1e400d2a64 | ||
|
|
967e63266f | ||
|
|
f9b0a88213 | ||
|
|
d67afa49b4 | ||
|
|
1fd170b089 | ||
|
|
a632718e80 | ||
|
|
9f72d05749 | ||
|
|
c062813c6d | ||
|
|
3ae1638125 | ||
|
|
96aff38862 | ||
|
|
ed3fb1efa4 | ||
|
|
d689bbf38e | ||
|
|
c1bbdaf9ae | ||
|
|
ab9e1013b2 | ||
|
|
e1140b1bea | ||
|
|
cfd27c8d87 | ||
|
|
a18947eed2 | ||
|
|
f8592e0d5b | ||
|
|
5bfdf05ff2 | ||
|
|
9e851542ec | ||
|
|
e79946b4e4 | ||
|
|
aed212d8f8 | ||
|
|
c5d3c7f390 | ||
|
|
b047730830 | ||
|
|
9c7d4ef1f7 | ||
|
|
12d3c59172 | ||
|
|
ef06b7d5d0 | ||
|
|
f17e17a6b9 | ||
|
|
faf363cfd2 | ||
|
|
dbbd9179b7 | ||
|
|
49f848a453 | ||
|
|
ef56aea74f | ||
|
|
cb5fa85ac2 | ||
|
|
11bdd3cfcd | ||
|
|
f0dcc91907 | ||
|
|
c1c2d26ec7 | ||
|
|
93133b9a6c | ||
|
|
245f08055f | ||
|
|
00ddd63372 | ||
|
|
1765c7bbf4 | ||
|
|
65edd55516 | ||
|
|
4947cf8718 | ||
|
|
65dd2b8993 | ||
|
|
ef82cfa034 | ||
|
|
1a69d525af | ||
|
|
307827be3c | ||
|
|
40cb59336f | ||
|
|
a9e0ea8520 | ||
|
|
baeee642dd | ||
|
|
416efe9fd3 | ||
|
|
8b5ac390d1 | ||
|
|
212e8e7559 | ||
|
|
41a20b50ea | ||
|
|
3742b51d58 | ||
|
|
1a21dff5d4 | ||
|
|
bbf7d9e08a | ||
|
|
ffed29e632 | ||
|
|
0f6538c1a7 | ||
|
|
ff2e055a5a | ||
|
|
cdf97f8717 | ||
|
|
b2af79a3c5 | ||
|
|
74cc5abd09 | ||
|
|
32c4712d5e | ||
|
|
3244395bfb | ||
|
|
1cb0e1ce7b | ||
|
|
42394fcbdd | ||
|
|
0b32e741f7 | ||
|
|
80c5d59916 | ||
|
|
e95823f543 | ||
|
|
06fe972683 | ||
|
|
3057396c02 | ||
|
|
7db9543fee | ||
|
|
58d86acf0d | ||
|
|
859020583d | ||
|
|
0cab620ba5 | ||
|
|
4338fcc51a | ||
|
|
5f6f1e8d36 | ||
|
|
a91f244f35 | ||
|
|
82bf04da81 | ||
|
|
a679e4a5e3 | ||
|
|
3c79404534 | ||
|
|
ba707d1149 | ||
|
|
93d88f30b4 | ||
|
|
f5bc136b07 | ||
|
|
ae69cbb207 | ||
|
|
39e3da1eb0 | ||
|
|
c1322b47c3 | ||
|
|
67f83bd5dd | ||
|
|
e424d01f3d | ||
|
|
a424830893 | ||
|
|
3c5810cc01 | ||
|
|
30bd4e1cef | ||
|
|
7956953669 | ||
|
|
dcba4615a2 | ||
|
|
0bf9de8256 | ||
|
|
77f1c7e74c | ||
|
|
27478946ea | ||
|
|
1f25a8af86 | ||
|
|
d75caad71f | ||
|
|
adf0226641 | ||
|
|
137f58a84a | ||
|
|
7c45a68870 | ||
|
|
99edab4b61 | ||
|
|
e50b72622c | ||
|
|
60dc40f47f | ||
|
|
841c331981 | ||
|
|
4eafa5a585 | ||
|
|
5a740e891e | ||
|
|
1fcc7001bd | ||
|
|
e57854422a | ||
|
|
8c39979848 | ||
|
|
2c38648e39 | ||
|
|
97aa739d69 | ||
|
|
b0042f29fb | ||
|
|
e3ca82945f | ||
|
|
bf6a3a7067 | ||
|
|
d25670c79a | ||
|
|
32b26e4ad3 | ||
|
|
818439db48 | ||
|
|
e23a9da1a8 | ||
|
|
37ebac2a9e | ||
|
|
46bf552afc | ||
|
|
70151e3dd8 | ||
|
|
e933f0baf2 | ||
|
|
f2a612c3d9 | ||
|
|
4a648f0068 | ||
|
|
5b52742cf7 | ||
|
|
237d234277 | ||
|
|
ed0cba281f | ||
|
|
2e0eaed322 | ||
|
|
e2a6d66805 | ||
|
|
8d6de9ca59 | ||
|
|
db108d964b | ||
|
|
f016d453fa | ||
|
|
60ea8d2c2b | ||
|
|
12ff1319f1 | ||
|
|
f224d8872e | ||
|
|
5cf2d5f062 | ||
|
|
92dd0ee1dd | ||
|
|
70c20fc76f | ||
|
|
07e0b5ac10 | ||
|
|
12f7fc3d33 | ||
|
|
60f47cb549 | ||
|
|
d33fa3f073 | ||
|
|
2e4fafcf46 | ||
|
|
ab451b9056 | ||
|
|
bc875a35b0 | ||
|
|
8e12a34634 | ||
|
|
77204127f2 | ||
|
|
65c2ccdc93 | ||
|
|
964d4f1f87 | ||
|
|
f559e9c74a | ||
|
|
610009528b | ||
|
|
0f10a88b23 | ||
|
|
60049c8cc5 | ||
|
|
50aa5880de | ||
|
|
47143318ba | ||
|
|
c27791a9ac | ||
|
|
b19d732a3a | ||
|
|
cd3db3a686 | ||
|
|
35fb9f8897 | ||
|
|
ec042434be | ||
|
|
f8041a3de5 | ||
|
|
dd90096e13 | ||
|
|
9ab5512bfa | ||
|
|
32ab56f864 | ||
|
|
78d7bfac01 | ||
|
|
57570c3ba6 | ||
|
|
ffac670f95 | ||
|
|
be16f1be44 | ||
|
|
fd0f85d565 | ||
|
|
8de5f3f0d3 | ||
|
|
0bb537b872 | ||
|
|
987da00be0 | ||
|
|
e9e2214d29 | ||
|
|
ac9f3317f1 | ||
|
|
7da85d277e | ||
|
|
274244b055 | ||
|
|
8fa611daed | ||
|
|
64d0fb17f7 | ||
|
|
6d1d844b14 | ||
|
|
686dd11d8e | ||
|
|
9d42ee9df8 | ||
|
|
9562768a04 | ||
|
|
54b8daede4 | ||
|
|
bd51afe86c | ||
|
|
a84b9bd2c8 | ||
|
|
ce1dac3b86 | ||
|
|
2dcd9f02cd | ||
|
|
416d57bec6 |
@@ -1,8 +1,14 @@
|
|||||||
[target.x86_64-pc-windows-msvc]
|
[target.x86_64-pc-windows-msvc]
|
||||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||||
[target.i686-pc-windows-msvc]
|
[target.i686-pc-windows-msvc]
|
||||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
|
||||||
[target.'cfg(target_os="macos")']
|
[target.'cfg(target_os="macos")']
|
||||||
rustflags = [
|
rustflags = [
|
||||||
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
|
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
|
||||||
]
|
]
|
||||||
|
#[target.'cfg(target_os="linux")']
|
||||||
|
# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
|
||||||
|
# this is unlikely to help also, because the other so files still use libc dynamically
|
||||||
|
#rustflags = [
|
||||||
|
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
|
||||||
|
#]
|
||||||
|
|||||||
230
.github/workflows/build-macos-arm64.yml
vendored
230
.github/workflows/build-macos-arm64.yml
vendored
@@ -1,230 +0,0 @@
|
|||||||
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.5"
|
|
||||||
NDK_VERSION: "r26d"
|
|
||||||
#signing keys env variable checks
|
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
|
||||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
|
||||||
# To make a custom build with your own servers set the below secret values
|
|
||||||
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
|
||||||
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
|
||||||
API_SERVER: "${{ secrets.API_SERVER }}"
|
|
||||||
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
|
||||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-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
|
|
||||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -4,9 +4,9 @@ env:
|
|||||||
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
||||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2023.10.19
|
# vcpkg version: 2024.06.15
|
||||||
# for multiarch gcc compatibility
|
# for multiarch gcc compatibility
|
||||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -112,6 +112,8 @@ jobs:
|
|||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
|
libva-dev \
|
||||||
|
libvdpau-dev \
|
||||||
libxcb-randr0-dev \
|
libxcb-randr0-dev \
|
||||||
libxcb-shape0-dev \
|
libxcb-shape0-dev \
|
||||||
libxcb-xfixes0-dev \
|
libxcb-xfixes0-dev \
|
||||||
|
|||||||
39
.github/workflows/fdroid.yml
vendored
Normal file
39
.github/workflows/fdroid.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Fdroid version file generation
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml
|
||||||
|
# Finds latest release and transforms F-Droid version code from version as follows:
|
||||||
|
# X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A
|
||||||
|
update-fdroid-version-file:
|
||||||
|
name: Publish RustDesk version file for F-Droid updater
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Generate RustDesk version file
|
||||||
|
run: |
|
||||||
|
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||||
|
UPSTREAM_VERNAME="${GITHUB_REF##refs/tags/}"
|
||||||
|
UPSTREAM_VERNAME="${UPSTREAM_VERNAME##v}"
|
||||||
|
else
|
||||||
|
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
|
||||||
|
fi
|
||||||
|
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
|
||||||
|
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
||||||
|
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Publish RustDesk version file
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: "fdroid-version"
|
||||||
|
files: |
|
||||||
|
./rustdesk-version.txt
|
||||||
633
.github/workflows/flutter-build.yml
vendored
633
.github/workflows/flutter-build.yml
vendored
File diff suppressed because it is too large
Load Diff
84
.github/workflows/history.yml
vendored
84
.github/workflows/history.yml
vendored
@@ -1,84 +0,0 @@
|
|||||||
name: Flutter Windows History Build
|
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
|
||||||
|
|
||||||
env:
|
|
||||||
LLVM_VERSION: "10.0"
|
|
||||||
FLUTTER_VERSION: "3.16.9"
|
|
||||||
TAG_NAME: "tmp"
|
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
|
||||||
VERSION: "1.2.5"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-for-history-windows:
|
|
||||||
name: ${{ matrix.job.date }}
|
|
||||||
runs-on: ${{ matrix.job.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
job:
|
|
||||||
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
|
||||||
steps:
|
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ matrix.job.ref }}
|
|
||||||
|
|
||||||
- name: Install LLVM and Clang
|
|
||||||
uses: KyleMayes/install-llvm-action@v1
|
|
||||||
with:
|
|
||||||
version: ${{ env.LLVM_VERSION }}
|
|
||||||
|
|
||||||
- name: Install flutter
|
|
||||||
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
target: ${{ matrix.job.target }}
|
|
||||||
override: true
|
|
||||||
components: rustfmt
|
|
||||||
profile: minimal # minimal component installation (ie, no documentation)
|
|
||||||
|
|
||||||
- name: Install flutter rust bridge deps
|
|
||||||
run: |
|
|
||||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
|
||||||
Push-Location flutter ; flutter pub get ; Pop-Location
|
|
||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
|
||||||
uses: lukka/run-vcpkg@v11
|
|
||||||
with:
|
|
||||||
vcpkgDirectory: C:\vcpkg
|
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
|
||||||
|
|
||||||
- name: Install vcpkg dependencies
|
|
||||||
run: |
|
|
||||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build rustdesk
|
|
||||||
run: python3 .\build.py --portable --hwcodec --flutter
|
|
||||||
|
|
||||||
- name: Build self-extracted executable
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
pushd ./libs/portable
|
|
||||||
python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
|
|
||||||
popd
|
|
||||||
mkdir -p ./SignOutput
|
|
||||||
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ matrix.job.date }}-${{ matrix.job.target }}.exe
|
|
||||||
|
|
||||||
- name: Publish Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
|
||||||
files: |
|
|
||||||
./SignOutput/rustdesk-*.exe
|
|
||||||
416
.github/workflows/playground.yml
vendored
Normal file
416
.github/workflows/playground.yml
vendored
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
name: playground
|
||||||
|
|
||||||
|
on:
|
||||||
|
#schedule:
|
||||||
|
# schedule build every night
|
||||||
|
# - cron: "0/6 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||||
|
CARGO_NDK_VERSION: "3.1.2"
|
||||||
|
LLVM_VERSION: "15.0.6"
|
||||||
|
FLUTTER_VERSION: "3.22.2"
|
||||||
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
|
# for arm64 linux because official Dart SDK does not work
|
||||||
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
|
TAG_NAME: "nightly"
|
||||||
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
|
# vcpkg version: 2024.06.15
|
||||||
|
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||||
|
VERSION: "1.3.0"
|
||||||
|
NDK_VERSION: "r26d"
|
||||||
|
#signing keys env variable checks
|
||||||
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||||
|
# To make a custom build with your own servers set the below secret values
|
||||||
|
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
||||||
|
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
||||||
|
API_SERVER: "${{ secrets.API_SERVER }}"
|
||||||
|
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
||||||
|
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-for-macOS:
|
||||||
|
name: ${{ matrix.job.target }}
|
||||||
|
runs-on: ${{ matrix.job.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.13.9",
|
||||||
|
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231219"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.10.6",
|
||||||
|
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231219"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.10.6",
|
||||||
|
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231119"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.13.9",
|
||||||
|
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231119"
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Export GitHub Actions cache environment variables
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.job.ref }}
|
||||||
|
|
||||||
|
- name: Import the codesign cert
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
uses: apple-actions/import-codesign-certs@v1
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
||||||
|
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
||||||
|
keychain: rustdesk
|
||||||
|
|
||||||
|
- name: Check sign and import sign key
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
run: |
|
||||||
|
security default-keychain -s rustdesk.keychain
|
||||||
|
security find-identity -v
|
||||||
|
|
||||||
|
- name: Import notarize key
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
uses: timheuer/base64-to-file@v1.2
|
||||||
|
with:
|
||||||
|
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
|
||||||
|
fileName: rustdesk.json
|
||||||
|
fileDir: ${{ github.workspace }}
|
||||||
|
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
|
||||||
|
|
||||||
|
- name: Install rcodesign tool
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pushd /tmp
|
||||||
|
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
|
||||||
|
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
|
||||||
|
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Install build runtime
|
||||||
|
run: |
|
||||||
|
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
|
||||||
|
|
||||||
|
- name: Install flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version: ${{ matrix.job.flutter }}
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
|
targets: ${{ matrix.job.target }}
|
||||||
|
components: "rustfmt"
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: ${{ matrix.job.os }}
|
||||||
|
|
||||||
|
- name: Install flutter rust bridge deps
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
|
||||||
|
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
|
||||||
|
# below works for mac to make buildable on 3.13.9
|
||||||
|
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
|
||||||
|
pushd flutter && flutter pub get && popd
|
||||||
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||||
|
|
||||||
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
|
uses: lukka/run-vcpkg@v11
|
||||||
|
with:
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
run: |
|
||||||
|
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||||
|
|
||||||
|
- name: Restore from cache and install vcpkg
|
||||||
|
uses: lukka/run-vcpkg@v7
|
||||||
|
if: false
|
||||||
|
with:
|
||||||
|
setupOnly: true
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
if: false
|
||||||
|
run: |
|
||||||
|
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
|
||||||
|
|
||||||
|
- name: Show version information (Rust, cargo, Clang)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
clang --version || true
|
||||||
|
rustup -V
|
||||||
|
rustup toolchain list
|
||||||
|
rustup default
|
||||||
|
cargo -V
|
||||||
|
rustc -V
|
||||||
|
|
||||||
|
- name: Build rustdesk
|
||||||
|
run: |
|
||||||
|
./build.py --flutter ${{ matrix.job.extra-build-args }}
|
||||||
|
|
||||||
|
- name: create unsigned dmg
|
||||||
|
run: |
|
||||||
|
CREATE_DMG="$(command -v create-dmg)"
|
||||||
|
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||||
|
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||||
|
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||||
|
|
||||||
|
- name: Codesign app and create signed dmg
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
run: |
|
||||||
|
# Patch create-dmg to give more attempts to unmount image
|
||||||
|
CREATE_DMG="$(command -v create-dmg)"
|
||||||
|
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||||
|
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||||
|
# Unlock keychain
|
||||||
|
security default-keychain -s rustdesk.keychain
|
||||||
|
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
|
||||||
|
# start sign the rustdesk.app and dmg
|
||||||
|
rm -rf *.dmg || true
|
||||||
|
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
|
||||||
|
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||||
|
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
|
||||||
|
# notarize the rustdesk-${{ env.VERSION }}.dmg
|
||||||
|
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
|
||||||
|
|
||||||
|
- name: Rename rustdesk
|
||||||
|
run: |
|
||||||
|
for name in rustdesk*??.dmg; do
|
||||||
|
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Publish DMG package
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
rustdesk*-${{ matrix.job.arch }}*.dmg
|
||||||
|
|
||||||
|
|
||||||
|
build-rustdesk-android:
|
||||||
|
if: false
|
||||||
|
name: build rustdesk android apk ${{ matrix.job.target }}
|
||||||
|
runs-on: ${{ matrix.job.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
arch: aarch64,
|
||||||
|
target: aarch64-linux-android,
|
||||||
|
os: ubuntu-20.04,
|
||||||
|
openssl-arch: android-arm64,
|
||||||
|
ref: master, # latest
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.job.ref }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
clang \
|
||||||
|
cmake \
|
||||||
|
curl \
|
||||||
|
gcc-multilib \
|
||||||
|
git \
|
||||||
|
g++ \
|
||||||
|
g++-multilib \
|
||||||
|
libappindicator3-dev \
|
||||||
|
libasound2-dev \
|
||||||
|
libc6-dev \
|
||||||
|
libclang-10-dev \
|
||||||
|
libgstreamer1.0-dev \
|
||||||
|
libgstreamer-plugins-base1.0-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
libpulse-dev \
|
||||||
|
libva-dev \
|
||||||
|
libvdpau-dev \
|
||||||
|
libxcb-randr0-dev \
|
||||||
|
libxcb-shape0-dev \
|
||||||
|
libxcb-xfixes0-dev \
|
||||||
|
libxdo-dev \
|
||||||
|
libxfixes-dev \
|
||||||
|
llvm-10-dev \
|
||||||
|
nasm \
|
||||||
|
yasm \
|
||||||
|
ninja-build \
|
||||||
|
openjdk-11-jdk-headless \
|
||||||
|
pkg-config \
|
||||||
|
tree \
|
||||||
|
wget
|
||||||
|
|
||||||
|
- name: Install flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
|
components: "rustfmt"
|
||||||
|
|
||||||
|
- name: Install flutter rust bridge deps
|
||||||
|
run: |
|
||||||
|
git config --global core.longpaths true
|
||||||
|
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
|
||||||
|
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
||||||
|
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
|
||||||
|
pushd flutter ; flutter pub get ; popd
|
||||||
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
|
- uses: nttld/setup-ndk@v1
|
||||||
|
id: setup-ndk
|
||||||
|
with:
|
||||||
|
ndk-version: ${{ env.NDK_VERSION }}
|
||||||
|
add-to-path: true
|
||||||
|
|
||||||
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
|
uses: lukka/run-vcpkg@v11
|
||||||
|
with:
|
||||||
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
run: |
|
||||||
|
case ${{ matrix.job.target }} in
|
||||||
|
aarch64-linux-android)
|
||||||
|
./flutter/build_android_deps.sh arm64-v8a
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
./flutter/build_android_deps.sh armeabi-v7a
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Clone deps
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pushd /opt
|
||||||
|
git clone https://github.com/rustdesk-org/rustdesk_thirdparty_lib.git --depth=1
|
||||||
|
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
|
||||||
|
# cp -rf /opt/rustdesk_thirdparty_lib/vcpkg/* /opt/artifacts/vcpkg/
|
||||||
|
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
|
||||||
|
|
||||||
|
- name: Build rustdesk lib
|
||||||
|
env:
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
run: |
|
||||||
|
rustup target add ${{ matrix.job.target }}
|
||||||
|
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
|
||||||
|
case ${{ matrix.job.target }} in
|
||||||
|
aarch64-linux-android)
|
||||||
|
./flutter/ndk_arm64.sh
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
./flutter/ndk_arm.sh
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Build rustdesk
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
||||||
|
run: |
|
||||||
|
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
||||||
|
# temporary use debug sign config
|
||||||
|
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||||
|
case ${{ matrix.job.target }} in
|
||||||
|
aarch64-linux-android)
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||||
|
# build flutter
|
||||||
|
pushd flutter
|
||||||
|
flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||||
|
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
|
;;
|
||||||
|
armv7-linux-androideabi)
|
||||||
|
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
|
||||||
|
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||||
|
# build flutter
|
||||||
|
pushd flutter
|
||||||
|
flutter build apk --release --target-platform android-arm --split-per-abi
|
||||||
|
mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
popd
|
||||||
|
mkdir -p signed-apk; pushd signed-apk
|
||||||
|
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
|
||||||
|
|
||||||
|
- uses: r0adkll/sign-android-release@v1
|
||||||
|
name: Sign app APK
|
||||||
|
if: env.ANDROID_SIGNING_KEY != null
|
||||||
|
id: sign-rustdesk
|
||||||
|
with:
|
||||||
|
releaseDirectory: ./signed-apk
|
||||||
|
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ANDROID_ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
# override default build-tools version (29.0.3) -- optional
|
||||||
|
BUILD_TOOLS_VERSION: "30.0.2"
|
||||||
|
|
||||||
|
- name: Publish signed apk package
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
||||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -4,9 +4,9 @@ on:
|
|||||||
types: [released]
|
types: [released]
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: windows-latest # action can only be run on windows
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal2009/winget-releaser@v1
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
with:
|
with:
|
||||||
identifier: RustDesk.RustDesk
|
identifier: RustDesk.RustDesk
|
||||||
version: ${{ github.event.release.tag_name }}
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
|||||||
2722
Cargo.lock
generated
2722
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.5"
|
version = "1.3.0"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -66,7 +66,7 @@ default-net = "0.14"
|
|||||||
wol-rs = "1.0"
|
wol-rs = "1.0"
|
||||||
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
||||||
errno = "0.3"
|
errno = "0.3"
|
||||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
rdev = { git = "https://github.com/rustdesk-org/rdev" }
|
||||||
url = { version = "2.3", features = ["serde"] }
|
url = { version = "2.3", features = ["serde"] }
|
||||||
crossbeam-queue = "0.3"
|
crossbeam-queue = "0.3"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
@@ -89,7 +89,10 @@ sys-locale = "0.3"
|
|||||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||||
clipboard = { path = "libs/clipboard" }
|
clipboard = { path = "libs/clipboard" }
|
||||||
ctrlc = "3.2"
|
ctrlc = "3.2"
|
||||||
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
|
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
|
||||||
|
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
|
||||||
|
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||||
|
|
||||||
system_shutdown = "4.0"
|
system_shutdown = "4.0"
|
||||||
qrcode-generator = "4.1"
|
qrcode-generator = "4.1"
|
||||||
|
|
||||||
@@ -111,7 +114,7 @@ winapi = { version = "0.3", features = [
|
|||||||
winreg = "0.11"
|
winreg = "0.11"
|
||||||
windows-service = "0.6"
|
windows-service = "0.6"
|
||||||
virtual_display = { path = "libs/virtual_display" }
|
virtual_display = { path = "libs/virtual_display" }
|
||||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
|
||||||
shared_memory = "0.12"
|
shared_memory = "0.12"
|
||||||
tauri-winrt-notification = "0.1.2"
|
tauri-winrt-notification = "0.1.2"
|
||||||
runas = "1.2"
|
runas = "1.2"
|
||||||
@@ -135,7 +138,7 @@ image = "0.24"
|
|||||||
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
|
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||||
wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
|
wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||||
@@ -149,11 +152,10 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
|||||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||||
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
||||||
async-process = "1.7"
|
async-process = "1.7"
|
||||||
mouce = { git="https://github.com/fufesou/mouce.git" }
|
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||||
evdev = { git="https://github.com/fufesou/evdev" }
|
|
||||||
dbus = "0.9"
|
dbus = "0.9"
|
||||||
dbus-crossroads = "0.5"
|
dbus-crossroads = "0.5"
|
||||||
pam = { git="https://github.com/fufesou/pam" }
|
pam = { git="https://github.com/rustdesk-org/pam" }
|
||||||
users = { version = "0.11" }
|
users = { version = "0.11" }
|
||||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||||
@@ -163,7 +165,7 @@ once_cell = {version = "1.18", optional = true}
|
|||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.13"
|
android_logger = "0.13"
|
||||||
jni = "0.21"
|
jni = "0.21"
|
||||||
android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
|
android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
||||||
|
|||||||
@@ -59,19 +59,19 @@ Please download Sciter dynamic library yourself.
|
|||||||
```sh
|
```sh
|
||||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### openSUSE Tumbleweed
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.5
|
version: 1.3.0
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.5
|
version: 1.3.0
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
@@ -37,6 +37,9 @@ AppDir:
|
|||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
||||||
universe multiverse
|
universe multiverse
|
||||||
include:
|
include:
|
||||||
|
# https://github.com/rustdesk/rustdesk/issues/9103
|
||||||
|
# Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try.
|
||||||
|
# We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it.
|
||||||
- libc6:amd64
|
- libc6:amd64
|
||||||
- libgtk-3-0
|
- libgtk-3-0
|
||||||
- libxcb-randr0
|
- libxcb-randr0
|
||||||
|
|||||||
26
build.py
26
build.py
@@ -25,12 +25,17 @@ flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
|
|||||||
skip_cargo = False
|
skip_cargo = False
|
||||||
|
|
||||||
|
|
||||||
def get_arch() -> str:
|
def get_deb_arch() -> str:
|
||||||
custom_arch = os.environ.get("ARCH")
|
custom_arch = os.environ.get("DEB_ARCH")
|
||||||
if custom_arch is None:
|
if custom_arch is None:
|
||||||
return "amd64"
|
return "amd64"
|
||||||
return custom_arch
|
return custom_arch
|
||||||
|
|
||||||
|
def get_deb_extra_depends() -> str:
|
||||||
|
custom_arch = os.environ.get("DEB_ARCH")
|
||||||
|
if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so
|
||||||
|
return ", libatomic1"
|
||||||
|
return ""
|
||||||
|
|
||||||
def system2(cmd):
|
def system2(cmd):
|
||||||
exit_code = os.system(cmd)
|
exit_code = os.system(cmd)
|
||||||
@@ -48,15 +53,7 @@ def get_version():
|
|||||||
|
|
||||||
|
|
||||||
def parse_rc_features(feature):
|
def parse_rc_features(feature):
|
||||||
available_features = {
|
available_features = {}
|
||||||
'PrivacyMode': {
|
|
||||||
'platform': ['windows'],
|
|
||||||
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3'
|
|
||||||
'/TempTopMostWindow_x64.zip',
|
|
||||||
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3/checksum_md5',
|
|
||||||
'include': ['WindowInjection.dll'],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
apply_features = {}
|
apply_features = {}
|
||||||
if not feature:
|
if not feature:
|
||||||
feature = []
|
feature = []
|
||||||
@@ -81,7 +78,6 @@ def parse_rc_features(feature):
|
|||||||
elif isinstance(feature, list):
|
elif isinstance(feature, list):
|
||||||
if windows:
|
if windows:
|
||||||
# download third party is deprecated, we use github ci instead.
|
# download third party is deprecated, we use github ci instead.
|
||||||
# force add PrivacyMode
|
|
||||||
# feature.append('PrivacyMode')
|
# feature.append('PrivacyMode')
|
||||||
pass
|
pass
|
||||||
for feat in feature:
|
for feat in feature:
|
||||||
@@ -108,7 +104,7 @@ def make_parser():
|
|||||||
nargs='+',
|
nargs='+',
|
||||||
default='',
|
default='',
|
||||||
help='Integrate features, windows only.'
|
help='Integrate features, windows only.'
|
||||||
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
'Available: [Not used for now]. Special value is "ALL" and empty "". Default is empty.')
|
||||||
parser.add_argument('--flutter', action='store_true',
|
parser.add_argument('--flutter', action='store_true',
|
||||||
help='Build flutter package', default=False)
|
help='Build flutter package', default=False)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -291,10 +287,10 @@ Version: %s
|
|||||||
Architecture: %s
|
Architecture: %s
|
||||||
Maintainer: rustdesk <info@rustdesk.com>
|
Maintainer: rustdesk <info@rustdesk.com>
|
||||||
Homepage: https://rustdesk.com
|
Homepage: https://rustdesk.com
|
||||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
|
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire%s
|
||||||
Description: A remote control software.
|
Description: A remote control software.
|
||||||
|
|
||||||
""" % (version, get_arch())
|
""" % (version, get_deb_arch(), get_deb_extra_depends())
|
||||||
file = open(control_file_path, "w")
|
file = open(control_file_path, "w")
|
||||||
file.write(content)
|
file.write(content)
|
||||||
file.close()
|
file.close()
|
||||||
|
|||||||
@@ -1,60 +1,73 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
<img src="../res/logo-header.svg" alt="RustDesk - あなたのためのリモートデスクトップ"><br>
|
||||||
<a href="#free-public-servers">Servers</a> •
|
<a href="#free-public-servers">Servers</a> •
|
||||||
<a href="#raw-steps-to-build">Build</a> •
|
<a href="#raw-steps-to-build">Build</a> •
|
||||||
<a href="#how-to-build-with-docker">Docker</a> •
|
<a href="#how-to-build-with-docker">Docker</a> •
|
||||||
<a href="#file-structure">Structure</a> •
|
<a href="#file-structure">Structure</a> •
|
||||||
<a href="#snapshot">Snapshot</a><br>
|
<a href="#snapshot">Snapshot</a><br>
|
||||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
|
||||||
<b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b>
|
<b>READMEや<a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>、 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a>の翻訳者を歓迎します!</b>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||||
|
|
||||||
|
|
||||||
[](https://ko-fi.com/I2I04VU09)
|
[](https://ko-fi.com/I2I04VU09)
|
||||||
|
|
||||||
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。
|
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを作成する](https://github.com/rustdesk/rustdesk-server-demo)こともできます。
|
||||||
|
|
||||||

|

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

|

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

|

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

|

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

|

|
||||||
|
|||||||
@@ -106,5 +106,6 @@ dependencies {
|
|||||||
implementation "androidx.media:media:1.6.0"
|
implementation "androidx.media:media:1.6.0"
|
||||||
implementation 'com.github.getActivity:XXPermissions:18.5'
|
implementation 'com.github.getActivity:XXPermissions:18.5'
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||||
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,11 @@
|
|||||||
android:name=".MainService"
|
android:name=".MainService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaProjection" />
|
android:foregroundServiceType="mediaProjection" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".FloatingWindowService"
|
||||||
|
android:enabled="true" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Don't delete the meta-data below.
|
Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
||||||
|
|||||||
@@ -116,24 +116,20 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
if (!isSupportVoiceCall()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (isVideoStart() || isAudioStart()) {
|
// No need to check if video or audio is started here.
|
||||||
if (!switchToVoiceCall(mediaProjection)) {
|
if (!switchToVoiceCall(mediaProjection)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (!switchToVoiceCall(mediaProjection)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
// Return true if not supported, because is was not started.
|
||||||
return false
|
if (!isSupportVoiceCall()) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if (isVideoStart()) {
|
if (isVideoStart()) {
|
||||||
switchOutVoiceCall(mediaProjection)
|
switchOutVoiceCall(mediaProjection)
|
||||||
@@ -180,9 +176,6 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun tryReleaseAudio() {
|
fun tryReleaseAudio() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isAudioStart() || isVideoStart()) {
|
if (isAudioStart() || isVideoStart()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import com.caverock.androidsvg.SVG
|
||||||
|
import ffi.FFI
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class FloatingWindowService : Service(), View.OnTouchListener {
|
||||||
|
|
||||||
|
private lateinit var windowManager: WindowManager
|
||||||
|
private lateinit var layoutParams: WindowManager.LayoutParams
|
||||||
|
private lateinit var floatingView: ImageView
|
||||||
|
private lateinit var originalDrawable: Drawable
|
||||||
|
private lateinit var leftHalfDrawable: Drawable
|
||||||
|
private lateinit var rightHalfDrawable: Drawable
|
||||||
|
|
||||||
|
private var dragging = false
|
||||||
|
private var lastDownX = 0f
|
||||||
|
private var lastDownY = 0f
|
||||||
|
private var viewCreated = false;
|
||||||
|
private var keepScreenOn = KeepScreenOn.DURING_CONTROLLED
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logTag = "floatingService"
|
||||||
|
private var firstCreate = true
|
||||||
|
private var viewWidth = 120
|
||||||
|
private var viewHeight = 120
|
||||||
|
private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed
|
||||||
|
private const val MAX_VIEW_SIZE = 320
|
||||||
|
private var viewUntouchable = false
|
||||||
|
private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed
|
||||||
|
private var customSvg = ""
|
||||||
|
private var lastLayoutX = 0
|
||||||
|
private var lastLayoutY = 0
|
||||||
|
private var lastOrientation = Configuration.ORIENTATION_UNDEFINED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
|
try {
|
||||||
|
if (firstCreate) {
|
||||||
|
firstCreate = false
|
||||||
|
onFirstCreate(windowManager)
|
||||||
|
}
|
||||||
|
Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg")
|
||||||
|
createView(windowManager)
|
||||||
|
handler.postDelayed(runnable, 1000)
|
||||||
|
Log.d(logTag, "onCreate success")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(logTag, "onCreate failed: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (viewCreated) {
|
||||||
|
windowManager.removeView(floatingView)
|
||||||
|
}
|
||||||
|
handler.removeCallbacks(runnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun createView(windowManager: WindowManager) {
|
||||||
|
floatingView = ImageView(this)
|
||||||
|
viewCreated = true
|
||||||
|
originalDrawable = resources.getDrawable(R.drawable.floating_window, null)
|
||||||
|
if (customSvg.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val svg = SVG.getFromString(customSvg)
|
||||||
|
Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}");
|
||||||
|
// This make the svg render clear
|
||||||
|
svg.documentWidth = viewWidth * 1f
|
||||||
|
svg.documentHeight = viewHeight * 1f
|
||||||
|
originalDrawable = svg.renderToPicture().let {
|
||||||
|
BitmapDrawable(
|
||||||
|
resources,
|
||||||
|
Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888)
|
||||||
|
.also { bitmap ->
|
||||||
|
it.draw(Canvas(bitmap))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||||
|
Log.d(logTag, "custom svg loaded")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val originalBitmap = Bitmap.createBitmap(
|
||||||
|
originalDrawable.intrinsicWidth,
|
||||||
|
originalDrawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val canvas = Canvas(originalBitmap)
|
||||||
|
originalDrawable.setBounds(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
originalDrawable.draw(canvas)
|
||||||
|
val leftHalfBitmap = Bitmap.createBitmap(
|
||||||
|
originalBitmap,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
val rightHalfBitmap = Bitmap.createBitmap(
|
||||||
|
originalBitmap,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
0,
|
||||||
|
originalDrawable.intrinsicWidth / 2,
|
||||||
|
originalDrawable.intrinsicHeight
|
||||||
|
)
|
||||||
|
leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap)
|
||||||
|
rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap)
|
||||||
|
|
||||||
|
floatingView.setImageDrawable(rightHalfDrawable)
|
||||||
|
floatingView.setOnTouchListener(this)
|
||||||
|
floatingView.alpha = viewTransparency * 1f
|
||||||
|
|
||||||
|
var flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
|
||||||
|
if (viewUntouchable || viewTransparency == 0f) {
|
||||||
|
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||||
|
}
|
||||||
|
layoutParams = WindowManager.LayoutParams(
|
||||||
|
viewWidth / 2,
|
||||||
|
viewHeight,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
|
||||||
|
flags,
|
||||||
|
PixelFormat.TRANSLUCENT
|
||||||
|
)
|
||||||
|
|
||||||
|
layoutParams.gravity = Gravity.TOP or Gravity.START
|
||||||
|
layoutParams.x = lastLayoutX
|
||||||
|
layoutParams.y = lastLayoutY
|
||||||
|
|
||||||
|
val keepScreenOnOption = FFI.getLocalOption("keep-screen-on").lowercase()
|
||||||
|
keepScreenOn = when (keepScreenOnOption) {
|
||||||
|
"never" -> KeepScreenOn.NEVER
|
||||||
|
"service-on" -> KeepScreenOn.SERVICE_ON
|
||||||
|
else -> KeepScreenOn.DURING_CONTROLLED
|
||||||
|
}
|
||||||
|
Log.d(logTag, "keepScreenOn option: $keepScreenOnOption, value: $keepScreenOn")
|
||||||
|
updateKeepScreenOnLayoutParams()
|
||||||
|
|
||||||
|
windowManager.addView(floatingView, layoutParams)
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFirstCreate(windowManager: WindowManager) {
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
val w = wh.first
|
||||||
|
val h = wh.second
|
||||||
|
// size
|
||||||
|
FFI.getLocalOption("floating-window-size").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val size = it.toInt()
|
||||||
|
if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) {
|
||||||
|
viewWidth = size
|
||||||
|
viewHeight = size
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// untouchable
|
||||||
|
viewUntouchable = FFI.getLocalOption("floating-window-untouchable") == "Y"
|
||||||
|
// transparency
|
||||||
|
FFI.getLocalOption("floating-window-transparency").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val transparency = it.toInt()
|
||||||
|
if (transparency in 0..10) {
|
||||||
|
viewTransparency = transparency * 1f / 10
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// custom svg
|
||||||
|
FFI.getLocalOption("floating-window-svg").let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
customSvg = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// position
|
||||||
|
lastLayoutX = 0
|
||||||
|
lastLayoutY = (wh.second - viewHeight) / 2
|
||||||
|
lastOrientation = resources.configuration.orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun performClick() {
|
||||||
|
showPopupMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||||
|
when (event?.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
dragging = false
|
||||||
|
lastDownX = event.rawX
|
||||||
|
lastDownY = event.rawY
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
val clickDragTolerance = 10f
|
||||||
|
if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) {
|
||||||
|
performClick()
|
||||||
|
} else {
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
val dx = event.rawX - lastDownX
|
||||||
|
val dy = event.rawY - lastDownY
|
||||||
|
// ignore too small fist start moving(some time is click)
|
||||||
|
if (!dragging && dx*dx+dy*dy < 25) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dragging = true
|
||||||
|
layoutParams.x = event.rawX.toInt()
|
||||||
|
layoutParams.y = event.rawY.toInt()
|
||||||
|
layoutParams.width = viewWidth
|
||||||
|
floatingView.setImageDrawable(originalDrawable)
|
||||||
|
windowManager.updateViewLayout(view, layoutParams)
|
||||||
|
lastLayoutX = layoutParams.x
|
||||||
|
lastLayoutY = layoutParams.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveToScreenSide(center: Boolean = false) {
|
||||||
|
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
val w = wh.first
|
||||||
|
if (layoutParams.x < w / 2) {
|
||||||
|
layoutParams.x = 0
|
||||||
|
floatingView.setImageDrawable(rightHalfDrawable)
|
||||||
|
} else {
|
||||||
|
layoutParams.x = w - viewWidth / 2
|
||||||
|
floatingView.setImageDrawable(leftHalfDrawable)
|
||||||
|
}
|
||||||
|
if (center) {
|
||||||
|
layoutParams.y = (wh.second - viewHeight) / 2
|
||||||
|
}
|
||||||
|
layoutParams.width = viewWidth / 2
|
||||||
|
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||||
|
lastLayoutX = layoutParams.x
|
||||||
|
lastLayoutY = layoutParams.y
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
if (newConfig.orientation != lastOrientation) {
|
||||||
|
lastOrientation = newConfig.orientation
|
||||||
|
val wh = getScreenSize(windowManager)
|
||||||
|
Log.d(logTag, "orientation: $lastOrientation, screen size: ${wh.first} x ${wh.second}")
|
||||||
|
val newW = wh.first
|
||||||
|
val newH = wh.second
|
||||||
|
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
|
// Proportional change
|
||||||
|
layoutParams.x = (layoutParams.x.toFloat() / newH.toFloat() * newW.toFloat()).toInt()
|
||||||
|
layoutParams.y = (layoutParams.y.toFloat() / newW.toFloat() * newH.toFloat()).toInt()
|
||||||
|
}
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPopupMenu() {
|
||||||
|
val popupMenu = PopupMenu(this, floatingView)
|
||||||
|
val idShowRustDesk = 0
|
||||||
|
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
|
||||||
|
val idStopService = 1
|
||||||
|
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
|
||||||
|
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
idShowRustDesk -> {
|
||||||
|
openMainActivity()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
idStopService -> {
|
||||||
|
stopMainService()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupMenu.setOnDismissListener {
|
||||||
|
moveToScreenSide()
|
||||||
|
}
|
||||||
|
popupMenu.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun openMainActivity() {
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this, 0, intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
pendingIntent.send()
|
||||||
|
} catch (e: PendingIntent.CanceledException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopMainService() {
|
||||||
|
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class KeepScreenOn {
|
||||||
|
NEVER,
|
||||||
|
DURING_CONTROLLED,
|
||||||
|
SERVICE_ON,
|
||||||
|
}
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private val runnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (updateKeepScreenOnLayoutParams()) {
|
||||||
|
windowManager.updateViewLayout(floatingView, layoutParams)
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, 1000) // 1000 milliseconds = 1 second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateKeepScreenOnLayoutParams(): Boolean {
|
||||||
|
val oldOn = layoutParams.flags and FLAG_KEEP_SCREEN_ON != 0
|
||||||
|
val newOn = keepScreenOn == KeepScreenOn.SERVICE_ON || (keepScreenOn == KeepScreenOn.DURING_CONTROLLED && MainService.isStart)
|
||||||
|
if (oldOn != newOn) {
|
||||||
|
Log.d(logTag, "change keep screen on to $newOn")
|
||||||
|
if (newOn) {
|
||||||
|
layoutParams.flags = layoutParams.flags or FLAG_KEEP_SCREEN_ON
|
||||||
|
} else {
|
||||||
|
layoutParams.flags = layoutParams.flags and FLAG_KEEP_SCREEN_ON.inv()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -18,7 +18,9 @@ import android.widget.EditText
|
|||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import android.view.ViewGroup.LayoutParams
|
import android.view.ViewGroup.LayoutParams
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
|
import android.view.KeyEvent as KeyEventAndroid
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.media.AudioManager
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo
|
import android.accessibilityservice.AccessibilityServiceInfo
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||||
@@ -75,6 +77,8 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
private var fakeEditTextForTextStateCalculation: EditText? = null
|
private var fakeEditTextForTextStateCalculation: EditText? = null
|
||||||
|
|
||||||
|
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
||||||
val x = max(0, _x)
|
val x = max(0, _x)
|
||||||
@@ -294,6 +298,18 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
||||||
|
|
||||||
|
var ke: KeyEventAndroid? = null
|
||||||
|
if (Build.VERSION.SDK_INT < 33 || textToCommit == null) {
|
||||||
|
ke = KeyEventConverter.toAndroidKeyEvent(keyEvent)
|
||||||
|
}
|
||||||
|
ke?.let { event ->
|
||||||
|
if (tryHandleVolumeKeyEvent(event)) {
|
||||||
|
return
|
||||||
|
} else if (tryHandlePowerKeyEvent(event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 33) {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
getInputMethod()?.let { inputMethod ->
|
getInputMethod()?.let { inputMethod ->
|
||||||
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
||||||
@@ -302,7 +318,7 @@ class InputService : AccessibilityService() {
|
|||||||
inputConnection.commitText(text, 1, null)
|
inputConnection.commitText(text, 1, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event ->
|
ke?.let { event ->
|
||||||
inputConnection.sendKeyEvent(event)
|
inputConnection.sendKeyEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +327,7 @@ class InputService : AccessibilityService() {
|
|||||||
} else {
|
} else {
|
||||||
val handler = Handler(Looper.getMainLooper())
|
val handler = Handler(Looper.getMainLooper())
|
||||||
handler.post {
|
handler.post {
|
||||||
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event ->
|
ke?.let { event ->
|
||||||
val possibleNodes = possibleAccessibiltyNodes()
|
val possibleNodes = possibleAccessibiltyNodes()
|
||||||
Log.d(logTag, "possibleNodes:$possibleNodes")
|
Log.d(logTag, "possibleNodes:$possibleNodes")
|
||||||
for (item in possibleNodes) {
|
for (item in possibleNodes) {
|
||||||
@@ -325,6 +341,43 @@ class InputService : AccessibilityService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tryHandleVolumeKeyEvent(event: KeyEventAndroid): Boolean {
|
||||||
|
when (event.keyCode) {
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_UP -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.raiseVolume(null, true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_DOWN -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.lowerVolume(null, true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_MUTE -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.toggleMute(true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryHandlePowerKeyEvent(event: KeyEventAndroid): Boolean {
|
||||||
|
if (event.keyCode == KeyEventAndroid.KEYCODE_POWER) {
|
||||||
|
// Perform power dialog action when action is up
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||||
|
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
|
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
return
|
return
|
||||||
@@ -422,7 +475,7 @@ class InputService : AccessibilityService() {
|
|||||||
return linkedList
|
return linkedList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
||||||
node.refresh()
|
node.refresh()
|
||||||
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
||||||
this.fakeEditTextForTextStateCalculation?.setText(null)
|
this.fakeEditTextForTextStateCalculation?.setText(null)
|
||||||
@@ -487,10 +540,10 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
||||||
it.onPreDraw()
|
it.onPreDraw()
|
||||||
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
val succ = it.onKeyDown(event.getKeyCode(), event)
|
val succ = it.onKeyDown(event.getKeyCode(), event)
|
||||||
Log.d(logTag, "onKeyDown $succ")
|
Log.d(logTag, "onKeyDown $succ")
|
||||||
} else if (event.action == android.view.KeyEvent.ACTION_UP) {
|
} else if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||||
val success = it.onKeyUp(event.getKeyCode(), event)
|
val success = it.onKeyUp(event.getKeyCode(), event)
|
||||||
Log.d(logTag, "keyup $success")
|
Log.d(logTag, "keyup $success")
|
||||||
} else {}
|
} else {}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ object KeyEventConverter {
|
|||||||
action = KeyEvent.ACTION_UP
|
action = KeyEvent.ACTION_UP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: The last parameter is the repeat count, not modifiers ?
|
||||||
|
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
|
||||||
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +114,10 @@ object KeyEventConverter {
|
|||||||
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
||||||
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
||||||
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
||||||
|
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
|
||||||
|
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
|
||||||
|
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
|
||||||
|
ControlKey.Power -> KeyEvent.KEYCODE_POWER
|
||||||
else -> 0 // Default to unknown.
|
else -> 0 // Default to unknown.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,6 +233,17 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GET_VALUE -> {
|
||||||
|
if (call.arguments is String) {
|
||||||
|
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
|
||||||
|
result.success(isSupportVoiceCall())
|
||||||
|
} else {
|
||||||
|
result.error("-1", "No such key", null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
"on_voice_call_started" -> {
|
"on_voice_call_started" -> {
|
||||||
onVoiceCallStarted()
|
onVoiceCallStarted()
|
||||||
}
|
}
|
||||||
@@ -252,19 +263,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
val codecArray = JSONArray()
|
val codecArray = JSONArray()
|
||||||
|
|
||||||
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
var w = 0
|
val wh = getScreenSize(windowManager)
|
||||||
var h = 0
|
var w = wh.first
|
||||||
@Suppress("DEPRECATION")
|
var h = wh.second
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
val m = windowManager.maximumWindowMetrics
|
|
||||||
w = m.bounds.width()
|
|
||||||
h = m.bounds.height()
|
|
||||||
} else {
|
|
||||||
val dm = DisplayMetrics()
|
|
||||||
windowManager.defaultDisplay.getRealMetrics(dm)
|
|
||||||
w = dm.widthPixels
|
|
||||||
h = dm.heightPixels
|
|
||||||
}
|
|
||||||
val align = 64
|
val align = 64
|
||||||
w = (w + align - 1) / align * align
|
w = (w + align - 1) / align * align
|
||||||
h = (h + align - 1) / align * align
|
h = (h + align - 1) / align * align
|
||||||
@@ -374,4 +375,17 @@ class MainActivity : FlutterActivity() {
|
|||||||
Log.d(logTag, "onVoiceCallClosed success")
|
Log.d(logTag, "onVoiceCallClosed success")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
val disableFloatingWindow = FFI.getLocalOption("disable-floating-window") == "Y"
|
||||||
|
if (!disableFloatingWindow && MainService.isReady) {
|
||||||
|
startService(Intent(this, FloatingWindowService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ class MainService : Service() {
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
|
fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
|
||||||
// turn on screen with LIFT_DOWN when screen off
|
// turn on screen with LIFT_DOWN when screen off
|
||||||
if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
|
if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
Log.d(logTag, "Turn on Screen, WakeLock release")
|
Log.d(logTag, "Turn on Screen, WakeLock release")
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
@@ -75,10 +75,10 @@ class MainService : Service() {
|
|||||||
wakeLock.acquire(5000)
|
wakeLock.acquire(5000)
|
||||||
} else {
|
} else {
|
||||||
when (kind) {
|
when (kind) {
|
||||||
"touch" -> {
|
0 -> { // touch
|
||||||
InputService.ctx?.onTouchInput(mask, x, y)
|
InputService.ctx?.onTouchInput(mask, x, y)
|
||||||
}
|
}
|
||||||
"mouse" -> {
|
1 -> { // mouse
|
||||||
InputService.ctx?.onMouseInput(mask, x, y)
|
InputService.ctx?.onMouseInput(mask, x, y)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -103,6 +103,9 @@ class MainService : Service() {
|
|||||||
put("scale",SCREEN_INFO.scale)
|
put("scale",SCREEN_INFO.scale)
|
||||||
}.toString()
|
}.toString()
|
||||||
}
|
}
|
||||||
|
"is_start" -> {
|
||||||
|
isStart.toString()
|
||||||
|
}
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,10 +175,10 @@ class MainService : Service() {
|
|||||||
Log.d(logTag, "from rust:stop_capture")
|
Log.d(logTag, "from rust:stop_capture")
|
||||||
stopCapture()
|
stopCapture()
|
||||||
}
|
}
|
||||||
"is_hardware_codec" -> {
|
"half_scale" -> {
|
||||||
val isHwCodec = arg1.toBoolean()
|
val halfScale = arg1.toBoolean()
|
||||||
if (isHardwareCodec != isHwCodec) {
|
if (isHalfScale != halfScale) {
|
||||||
isHardwareCodec = isHwCodec
|
isHalfScale = halfScale
|
||||||
updateScreenInfo(resources.configuration.orientation)
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,11 +194,6 @@ class MainService : Service() {
|
|||||||
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
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")}
|
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
||||||
|
|
||||||
private fun translate(input: String): String {
|
|
||||||
Log.d(logTag, "translate:$LOCAL_NAME")
|
|
||||||
return FFI.translateLocale(LOCAL_NAME, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var _isReady = false // media permission ready status
|
private var _isReady = false // media permission ready status
|
||||||
private var _isStart = false // screen capture start status
|
private var _isStart = false // screen capture start status
|
||||||
@@ -252,10 +250,11 @@ class MainService : Service() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isHardwareCodec: Boolean? = null;
|
private var isHalfScale: Boolean? = null;
|
||||||
private fun updateScreenInfo(orientation: Int) {
|
private fun updateScreenInfo(orientation: Int) {
|
||||||
var w: Int
|
var w: Int
|
||||||
var h: Int
|
var h: Int
|
||||||
@@ -288,7 +287,7 @@ class MainService : Service() {
|
|||||||
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||||
var scale = 1
|
var scale = 1
|
||||||
if (w != 0 && h != 0) {
|
if (w != 0 && h != 0) {
|
||||||
if (isHardwareCodec == false && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
if (isHalfScale == true && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
||||||
scale = 2
|
scale = 2
|
||||||
w /= scale
|
w /= scale
|
||||||
h /= scale
|
h /= scale
|
||||||
@@ -486,6 +485,7 @@ class MainService : Service() {
|
|||||||
mediaProjection = null
|
mediaProjection = null
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
|
stopService(Intent(this, FloatingWindowService::class.java))
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
// Inspired by https://github.com/yosemiteyss/flutter_volume_controller/blob/main/android/src/main/kotlin/com/yosemiteyss/flutter_volume_controller/VolumeController.kt
|
||||||
|
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
class VolumeController(private val audioManager: AudioManager) {
|
||||||
|
private val logTag = "volume controller"
|
||||||
|
|
||||||
|
fun getVolume(streamType: Int): Double {
|
||||||
|
val current = audioManager.getStreamVolume(streamType)
|
||||||
|
val max = audioManager.getStreamMaxVolume(streamType)
|
||||||
|
return current.toDouble() / max
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVolume(volume: Double, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
val max = audioManager.getStreamMaxVolume(streamType)
|
||||||
|
audioManager.setStreamVolume(
|
||||||
|
streamType,
|
||||||
|
(max * volume).toInt(),
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun raiseVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (step == null) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
AudioManager.ADJUST_RAISE,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val target = getVolume(streamType) + step
|
||||||
|
setVolume(target, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lowerVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (step == null) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
AudioManager.ADJUST_LOWER,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val target = getVolume(streamType) - step
|
||||||
|
setVolume(target, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMute(streamType: Int): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
audioManager.isStreamMute(streamType)
|
||||||
|
} else {
|
||||||
|
audioManager.getStreamVolume(streamType) == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMute(isMuted: Boolean, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
if (isMuted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
audioManager.setStreamMute(streamType, isMuted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleMute(showSystemUI: Boolean, streamType: Int) {
|
||||||
|
val isMuted = getMute(streamType)
|
||||||
|
setMute(!isMuted, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,10 +15,14 @@ import android.os.Looper
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.provider.Settings.*
|
import android.provider.Settings.*
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import com.hjq.permissions.Permission
|
import com.hjq.permissions.Permission
|
||||||
import com.hjq.permissions.XXPermissions
|
import com.hjq.permissions.XXPermissions
|
||||||
|
import ffi.FFI
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -43,6 +47,9 @@ const val START_ACTION = "start_action"
|
|||||||
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
||||||
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
||||||
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
||||||
|
const val GET_VALUE = "get_value"
|
||||||
|
|
||||||
|
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
|
||||||
|
|
||||||
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
|
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
|
||||||
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
|
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
|
||||||
@@ -56,6 +63,11 @@ data class Info(
|
|||||||
var width: Int, var height: Int, var scale: Int, var dpi: Int
|
var width: Int, var height: Int, var scale: Int, var dpi: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun isSupportVoiceCall(): Boolean {
|
||||||
|
// https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
}
|
||||||
|
|
||||||
fun requestPermission(context: Context, type: String) {
|
fun requestPermission(context: Context, type: String) {
|
||||||
XXPermissions.with(context)
|
XXPermissions.with(context)
|
||||||
.permission(type)
|
.permission(type)
|
||||||
@@ -120,3 +132,26 @@ class AudioReader(val bufSize: Int, private val maxFrames: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
|
||||||
|
var w = 0
|
||||||
|
var h = 0
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val m = windowManager.maximumWindowMetrics
|
||||||
|
w = m.bounds.width()
|
||||||
|
h = m.bounds.height()
|
||||||
|
} else {
|
||||||
|
val dm = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getRealMetrics(dm)
|
||||||
|
w = dm.widthPixels
|
||||||
|
h = dm.heightPixels
|
||||||
|
}
|
||||||
|
return Pair(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun translate(input: String): String {
|
||||||
|
Log.d("common", "translate:$LOCAL_NAME")
|
||||||
|
return FFI.translateLocale(LOCAL_NAME, input)
|
||||||
|
}
|
||||||
@@ -19,4 +19,5 @@ object FFI {
|
|||||||
external fun refreshScreen()
|
external fun refreshScreen()
|
||||||
external fun setFrameRawEnable(name: String, value: Boolean)
|
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||||
external fun setCodecInfo(info: String)
|
external fun setCodecInfo(info: String)
|
||||||
|
external fun getLocalOption(key: String): String
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:height="320dp" android:viewportHeight="32" android:viewportWidth="32" android:width="320dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#ffffff" android:pathData="M16,0L16,0A16,16 0,0 1,32 16L32,16A16,16 0,0 1,16 32L16,32A16,16 0,0 1,0 16L0,16A16,16 0,0 1,16 0z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#1a1a1a" android:pathData="m23.89,10.135 l-1.807,1.795c-0.318,0.285 -0.472,0.744 -0.293,1.131 1.204,2.518 0.747,5.52 -1.228,7.494 -1.976,1.973 -4.981,2.429 -7.502,1.226 -0.371,-0.166 -0.807,-0.025 -1.093,0.265l-1.836,1.833c-0.216,0.211 -0.322,0.51 -0.288,0.809 0.034,0.3 0.206,0.567 0.463,0.723 4.326,2.618 9.882,1.951 13.463,-1.618 3.581,-3.568 4.264,-9.115 1.655,-13.443 -0.15,-0.263 -0.414,-0.442 -0.714,-0.484 -0.3,-0.043 -0.603,0.058 -0.819,0.269zM8.265,8.184c-3.599,3.554 -4.304,9.103 -1.709,13.441 0.15,0.264 0.413,0.443 0.714,0.485 0.3,0.042 0.603,-0.058 0.82,-0.27l1.797,-1.785c0.325,-0.285 0.484,-0.749 0.303,-1.141 -1.204,-2.518 -0.748,-5.52 1.228,-7.493 1.975,-1.973 4.981,-2.429 7.502,-1.227 0.367,0.165 0.797,0.028 1.084,-0.254l1.846,-1.844c0.216,-0.211 0.322,-0.509 0.288,-0.809 -0.035,-0.299 -0.206,-0.566 -0.463,-0.723 -4.334,-2.596 -9.881,-1.908 -13.448,1.668z" android:strokeWidth="0.987992"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
563
flutter/build_fdroid.sh
Executable file
563
flutter/build_fdroid.sh
Executable file
@@ -0,0 +1,563 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
#
|
||||||
|
# Script to build F-Droid release of RustDesk
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024, The RustDesk Authors
|
||||||
|
# 2024, Vasyl Gello <vasek.gello@gmail.com>
|
||||||
|
#
|
||||||
|
|
||||||
|
# The script is invoked by F-Droid builder system ste-by-step.
|
||||||
|
#
|
||||||
|
# It accepts the following arguments:
|
||||||
|
#
|
||||||
|
# - versionName from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
|
||||||
|
# - versionCode from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
|
||||||
|
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
|
||||||
|
# - The build step to execute:
|
||||||
|
#
|
||||||
|
# + sudo-deps: as root, install needed Debian packages into builder VM
|
||||||
|
# + prebuild: patch sources and do other stuff before the build
|
||||||
|
# + build: perform actual build of APK file
|
||||||
|
#
|
||||||
|
|
||||||
|
# Parse command-line arguments
|
||||||
|
|
||||||
|
VERNAME="${1}"
|
||||||
|
VERCODE="${2}"
|
||||||
|
ANDROID_ABI="${3}"
|
||||||
|
BUILDSTEP="${4}"
|
||||||
|
|
||||||
|
if [ -z "${VERNAME}" ] || [ -z "${VERCODE}" ] || [ -z "${ANDROID_ABI}" ] ||
|
||||||
|
[ -z "${BUILDSTEP}" ]; then
|
||||||
|
echo "ERROR: Command-line arguments are all required to be non-empty!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set various architecture-specific identifiers
|
||||||
|
|
||||||
|
case "${ANDROID_ABI}" in
|
||||||
|
arm64-v8a)
|
||||||
|
FLUTTER_TARGET=android-arm64
|
||||||
|
NDK_TARGET=aarch64-linux-android
|
||||||
|
RUST_TARGET=aarch64-linux-android
|
||||||
|
RUSTDESK_FEATURES='flutter,hwcodec'
|
||||||
|
;;
|
||||||
|
armeabi-v7a)
|
||||||
|
FLUTTER_TARGET=android-arm
|
||||||
|
NDK_TARGET=arm-linux-androideabi
|
||||||
|
RUST_TARGET=armv7-linux-androideabi
|
||||||
|
RUSTDESK_FEATURES='flutter,hwcodec'
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
FLUTTER_TARGET=android-x64
|
||||||
|
NDK_TARGET=x86_64-linux-android
|
||||||
|
RUST_TARGET=x86_64-linux-android
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
x86)
|
||||||
|
FLUTTER_TARGET=android-x86
|
||||||
|
NDK_TARGET=i686-linux-android
|
||||||
|
RUST_TARGET=i686-linux-android
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown Android ABI '${ANDROID_ABI}'!" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check ANDROID_SDK_ROOT and sdkmanager present on PATH
|
||||||
|
|
||||||
|
if [ ! -d "${ANDROID_SDK_ROOT}" ] || ! command -v sdkmanager 1>/dev/null; then
|
||||||
|
echo "ERROR: Can not find Android SDK!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export necessary variables
|
||||||
|
|
||||||
|
export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
|
||||||
|
|
||||||
|
export VCPKG_ROOT="${HOME}/vcpkg"
|
||||||
|
|
||||||
|
# Now act depending on build step
|
||||||
|
|
||||||
|
# NOTE: F-Droid maintainers require explicit declaration of dependencies
|
||||||
|
# as root via `Builds.sudo` F-Droid metadata directive:
|
||||||
|
# https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695
|
||||||
|
|
||||||
|
case "${BUILDSTEP}" in
|
||||||
|
prebuild)
|
||||||
|
# prebuild: patch sources and do other stuff before the build
|
||||||
|
|
||||||
|
#
|
||||||
|
# Extract required versions for NDK, Rust, Flutter from
|
||||||
|
# '.github/workflows/flutter-build.yml'
|
||||||
|
#
|
||||||
|
|
||||||
|
CARGO_NDK_VERSION="$(yq -r \
|
||||||
|
.env.CARGO_NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.ANDROID_FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_RUST_BRIDGE_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
NDK_VERSION="$(yq -r \
|
||||||
|
.env.NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
RUST_VERSION="$(yq -r \
|
||||||
|
.env.RUST_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
VCPKG_COMMIT_ID="$(yq -r \
|
||||||
|
.env.VCPKG_COMMIT_ID \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
|
||||||
|
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
|
||||||
|
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
|
||||||
|
[ -z "${VCPKG_COMMIT_ID}" ]; then
|
||||||
|
echo "ERROR: Can not identify all required versions!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Map NDK version to revision
|
||||||
|
|
||||||
|
NDK_VERSION="$(wget \
|
||||||
|
-qO- \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
'https://api.github.com/repos/android/ndk/releases' |
|
||||||
|
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||||
|
|
||||||
|
if [ -z "${NDK_VERSION}" ]; then
|
||||||
|
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Install the components
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install Android NDK
|
||||||
|
|
||||||
|
if [ ! -d "${ANDROID_NDK_ROOT}" ]; then
|
||||||
|
sdkmanager --install "ndk;${NDK_VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Flutter
|
||||||
|
|
||||||
|
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone https://github.com/flutter/flutter
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
git reset --hard "${FLUTTER_VERSION}"
|
||||||
|
|
||||||
|
flutter config --no-analytics
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Rust
|
||||||
|
|
||||||
|
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone --depth 1 https://github.com/rust-lang/rustup
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd "${HOME}/rustup"
|
||||||
|
bash rustup-init.sh -y \
|
||||||
|
--target "${RUST_TARGET}" \
|
||||||
|
--default-toolchain "${RUST_VERSION}"
|
||||||
|
popd
|
||||||
|
|
||||||
|
if ! command -v cargo 1>/dev/null 2>&1; then
|
||||||
|
. "${HOME}/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install cargo-ndk
|
||||||
|
|
||||||
|
cargo install \
|
||||||
|
cargo-ndk \
|
||||||
|
--version "${CARGO_NDK_VERSION}"
|
||||||
|
|
||||||
|
# Install rust bridge generator
|
||||||
|
|
||||||
|
cargo install cargo-expand
|
||||||
|
cargo install flutter_rust_bridge_codegen \
|
||||||
|
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
|
||||||
|
--features "uuid"
|
||||||
|
|
||||||
|
# Populate native vcpkg dependencies
|
||||||
|
|
||||||
|
if [ ! -d "${VCPKG_ROOT}" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
https://github.com/Microsoft/vcpkg.git
|
||||||
|
git clone \
|
||||||
|
https://github.com/Microsoft/vcpkg-tool.git
|
||||||
|
|
||||||
|
pushd vcpkg-tool
|
||||||
|
|
||||||
|
mkdir build
|
||||||
|
|
||||||
|
pushd build
|
||||||
|
|
||||||
|
cmake \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-G 'Ninja' \
|
||||||
|
-DVCPKG_DEVELOPMENT_WARNINGS=OFF \
|
||||||
|
..
|
||||||
|
|
||||||
|
cmake --build .
|
||||||
|
|
||||||
|
popd # build
|
||||||
|
|
||||||
|
popd # vcpkg-tool
|
||||||
|
|
||||||
|
pushd vcpkg
|
||||||
|
|
||||||
|
git reset --hard "${VCPKG_COMMIT_ID}"
|
||||||
|
|
||||||
|
cp -a ../vcpkg-tool/build/vcpkg vcpkg
|
||||||
|
|
||||||
|
# disable telemetry
|
||||||
|
|
||||||
|
touch "vcpkg.disable-metrics"
|
||||||
|
|
||||||
|
popd # vcpkg
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install depot-tools for x86
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
if [ ! -d "${HOME}/depot_tools" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
--depth 1 \
|
||||||
|
https://chromium.googlesource.com/chromium/tools/depot_tools.git
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Patch the RustDesk sources
|
||||||
|
|
||||||
|
git apply res/fdroid/patches/*.patch
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/gms/d' \
|
||||||
|
flutter/android/build.gradle \
|
||||||
|
flutter/android/app/build.gradle
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/firebase_analytics/d' \
|
||||||
|
flutter/pubspec.yaml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/ firebase/,/ version/d' \
|
||||||
|
flutter/pubspec.lock
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/firebase/Id' \
|
||||||
|
flutter/lib/main.dart
|
||||||
|
|
||||||
|
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
|
||||||
|
# Fix for android 3.13.9
|
||||||
|
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e 's/uni_links_desktop/#uni_links_desktop/g' \
|
||||||
|
flutter/pubspec.yaml
|
||||||
|
|
||||||
|
set --
|
||||||
|
|
||||||
|
while read -r _1; do
|
||||||
|
set -- "$@" "${_1}"
|
||||||
|
done 0<<.a
|
||||||
|
$(find flutter/lib/ -type f -name "*dart*")
|
||||||
|
.a
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
set --
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
|
||||||
|
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
# build: perform actual build of APK file
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#
|
||||||
|
# Extract required versions for NDK, Rust, Flutter from
|
||||||
|
# '.github/workflows/flutter-build.yml'
|
||||||
|
#
|
||||||
|
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.ANDROID_FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
NDK_VERSION="$(yq -r \
|
||||||
|
.env.NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
# Map NDK version to revision
|
||||||
|
|
||||||
|
NDK_VERSION="$(wget \
|
||||||
|
-qO- \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
'https://api.github.com/repos/android/ndk/releases' |
|
||||||
|
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||||
|
|
||||||
|
if [ -z "${NDK_VERSION}" ]; then
|
||||||
|
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
|
||||||
|
if ! command -v cargo 1>/dev/null 2>&1; then
|
||||||
|
. "${HOME}/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download Flutter dependencies
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
flutter packages pub get
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
# Generate FFI bindings
|
||||||
|
|
||||||
|
flutter_rust_bridge_codegen \
|
||||||
|
--rust-input ./src/flutter_ffi.rs \
|
||||||
|
--dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
|
# Build host android deps
|
||||||
|
|
||||||
|
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
|
||||||
|
|
||||||
|
# Build rustdesk lib
|
||||||
|
|
||||||
|
cargo ndk \
|
||||||
|
--platform 21 \
|
||||||
|
--target "${RUST_TARGET}" \
|
||||||
|
--bindgen \
|
||||||
|
build \
|
||||||
|
--release \
|
||||||
|
--features "${RUSTDESK_FEATURES}"
|
||||||
|
|
||||||
|
mkdir -p "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"
|
||||||
|
|
||||||
|
cp "target/${RUST_TARGET}/release/liblibrustdesk.so" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so"
|
||||||
|
|
||||||
|
cp "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/${NDK_TARGET}/libc++_shared.so" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/"
|
||||||
|
|
||||||
|
"${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"/*
|
||||||
|
|
||||||
|
# Build flutter-jit-release for x86
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
pushd flutter-sdk
|
||||||
|
|
||||||
|
echo "## Sync flutter engine sources"
|
||||||
|
echo "### We need fakeroot because chromium base image is unpacked with weird uid/gid ownership"
|
||||||
|
|
||||||
|
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" .gclient
|
||||||
|
|
||||||
|
export FAKEROOTDONTTRYCHOWN=1
|
||||||
|
|
||||||
|
fakeroot gclient sync
|
||||||
|
|
||||||
|
unset FAKEROOTDONTTRYCHOWN
|
||||||
|
|
||||||
|
pushd src
|
||||||
|
|
||||||
|
echo "## Patch away Google Play dependencies"
|
||||||
|
|
||||||
|
rm \
|
||||||
|
flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java \
|
||||||
|
flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/PlayStore/d' \
|
||||||
|
flutter/tools/android_lint/project.xml \
|
||||||
|
flutter/shell/platform/android/BUILD.gn
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/com.google.android.play/d' \
|
||||||
|
flutter/tools/androidx/files.json
|
||||||
|
|
||||||
|
echo "## Configure android engine build"
|
||||||
|
|
||||||
|
flutter/tools/gn \
|
||||||
|
--android --android-cpu x86 --runtime-mode=jit_release \
|
||||||
|
--no-goma --no-enable-unittests
|
||||||
|
|
||||||
|
echo "## Perform android engine build"
|
||||||
|
|
||||||
|
ninja -C out/android_jit_release_x86
|
||||||
|
|
||||||
|
echo "## Configure host engine build"
|
||||||
|
|
||||||
|
flutter/tools/gn \
|
||||||
|
--android-cpu x86 --runtime-mode=jit_release \
|
||||||
|
--no-goma --no-enable-unittests
|
||||||
|
|
||||||
|
echo "## Perform android engine build"
|
||||||
|
|
||||||
|
ninja -C out/host_jit_release_x86
|
||||||
|
|
||||||
|
echo "## Rename host engine"
|
||||||
|
|
||||||
|
mv out/host_jit_release_x86 out/host_jit_release
|
||||||
|
|
||||||
|
echo "## Mimic jit_release engine to debug to use with flutter build apk"
|
||||||
|
|
||||||
|
pushd out/android_jit_release_x86
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
flutter_embedding_jit_release.maven-metadata.xml \
|
||||||
|
1>flutter_embedding_debug.maven-metadata.xml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
flutter_embedding_jit_release.pom \
|
||||||
|
1>flutter_embedding_debug.pom
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
x86_jit_release.maven-metadata.xml \
|
||||||
|
1>x86_debug.maven-metadata.xml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
x86_jit_release.pom \
|
||||||
|
1>x86_debug.pom
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
flutter_embedding_jit_release-sources.jar \
|
||||||
|
flutter_embedding_debug-sources.jar
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
flutter_embedding_jit_release.jar \
|
||||||
|
flutter_embedding_debug.jar
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
x86_jit_release.jar \
|
||||||
|
x86_debug.jar
|
||||||
|
|
||||||
|
popd # out/android_jit_release_x86
|
||||||
|
|
||||||
|
popd # src
|
||||||
|
|
||||||
|
popd # flutter-sdk
|
||||||
|
|
||||||
|
echo "# Clean up intermediate engine files and show free space"
|
||||||
|
|
||||||
|
rm -rf \
|
||||||
|
flutter-sdk/src/out/android_jit_release_x86/obj \
|
||||||
|
flutter-sdk/src/out/host_jit_release/obj
|
||||||
|
|
||||||
|
mv flutter-sdk/src/out flutter-out
|
||||||
|
|
||||||
|
rm -rf flutter-sdk
|
||||||
|
|
||||||
|
mkdir -p flutter-sdk/src/
|
||||||
|
|
||||||
|
mv flutter-out flutter-sdk/src/out
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the apk
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
flutter build apk \
|
||||||
|
--local-engine-src-path="$(readlink -mf "../flutter-sdk/src")" \
|
||||||
|
--local-engine=android_jit_release_x86 \
|
||||||
|
--debug \
|
||||||
|
--build-number="${VERCODE}" \
|
||||||
|
--build-name="${VERNAME}" \
|
||||||
|
--target-platform "${FLUTTER_TARGET}"
|
||||||
|
else
|
||||||
|
flutter build apk \
|
||||||
|
--release \
|
||||||
|
--build-number="${VERCODE}" \
|
||||||
|
--build-name="${VERNAME}" \
|
||||||
|
--target-platform "${FLUTTER_TARGET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
rm -rf flutter-sdk
|
||||||
|
|
||||||
|
# Special step for fdroiddata CI builds to remove .gitconfig
|
||||||
|
|
||||||
|
rm -f /home/vagrant/.gitconfig
|
||||||
|
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown build step '${BUILDSTEP}'!" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Report success
|
||||||
|
|
||||||
|
echo "All done!"
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
<key>com.apple.developer.networking.wifi-info</key>
|
<key>com.apple.developer.networking.wifi-info</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo build --features flutter --release --target aarch64-apple-ios --lib
|
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import 'mobile/pages/file_manager_page.dart';
|
|||||||
import 'mobile/pages/remote_page.dart';
|
import 'mobile/pages/remote_page.dart';
|
||||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||||
import 'models/input_model.dart';
|
|
||||||
import 'models/model.dart';
|
import 'models/model.dart';
|
||||||
import 'models/platform_model.dart';
|
import 'models/platform_model.dart';
|
||||||
|
|
||||||
@@ -630,10 +629,30 @@ List<Locale> supportedLocales = const [
|
|||||||
Locale('da'),
|
Locale('da'),
|
||||||
Locale('eo'),
|
Locale('eo'),
|
||||||
Locale('tr'),
|
Locale('tr'),
|
||||||
Locale('vi'),
|
|
||||||
Locale('pl'),
|
|
||||||
Locale('kz'),
|
Locale('kz'),
|
||||||
Locale('es'),
|
Locale('es'),
|
||||||
|
Locale('nl'),
|
||||||
|
Locale('nb'),
|
||||||
|
Locale('et'),
|
||||||
|
Locale('eu'),
|
||||||
|
Locale('bg'),
|
||||||
|
Locale('be'),
|
||||||
|
Locale('vn'),
|
||||||
|
Locale('uk'),
|
||||||
|
Locale('fa'),
|
||||||
|
Locale('ca'),
|
||||||
|
Locale('el'),
|
||||||
|
Locale('sv'),
|
||||||
|
Locale('sq'),
|
||||||
|
Locale('sr'),
|
||||||
|
Locale('th'),
|
||||||
|
Locale('sl'),
|
||||||
|
Locale('ro'),
|
||||||
|
Locale('lt'),
|
||||||
|
Locale('lv'),
|
||||||
|
Locale('ar'),
|
||||||
|
Locale('he'),
|
||||||
|
Locale('hr'),
|
||||||
];
|
];
|
||||||
|
|
||||||
String formatDurationToTime(Duration duration) {
|
String formatDurationToTime(Duration duration) {
|
||||||
@@ -647,8 +666,12 @@ String formatDurationToTime(Duration duration) {
|
|||||||
|
|
||||||
closeConnection({String? id}) {
|
closeConnection({String? id}) {
|
||||||
if (isAndroid || isIOS) {
|
if (isAndroid || isIOS) {
|
||||||
|
() async {
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
|
overlays: SystemUiOverlay.values);
|
||||||
gFFI.chatModel.hideChatOverlay();
|
gFFI.chatModel.hideChatOverlay();
|
||||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||||
|
}();
|
||||||
} else {
|
} else {
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||||
@@ -718,7 +741,21 @@ class OverlayDialogManager {
|
|||||||
int _tagCount = 0;
|
int _tagCount = 0;
|
||||||
|
|
||||||
OverlayEntry? _mobileActionsOverlayEntry;
|
OverlayEntry? _mobileActionsOverlayEntry;
|
||||||
RxBool mobileActionsOverlayVisible = false.obs;
|
RxBool mobileActionsOverlayVisible = true.obs;
|
||||||
|
|
||||||
|
setMobileActionsOverlayVisible(bool v, {store = true}) {
|
||||||
|
if (store) {
|
||||||
|
bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N');
|
||||||
|
}
|
||||||
|
// No need to read the value from local storage after setting it.
|
||||||
|
// It better to toggle the value directly.
|
||||||
|
mobileActionsOverlayVisible.value = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMobileActionsOverlayVisible() {
|
||||||
|
mobileActionsOverlayVisible.value =
|
||||||
|
bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N';
|
||||||
|
}
|
||||||
|
|
||||||
void setOverlayState(OverlayKeyState overlayKeyState) {
|
void setOverlayState(OverlayKeyState overlayKeyState) {
|
||||||
_overlayKeyState = overlayKeyState;
|
_overlayKeyState = overlayKeyState;
|
||||||
@@ -865,14 +902,14 @@ class OverlayDialogManager {
|
|||||||
);
|
);
|
||||||
overlayState.insert(overlay);
|
overlayState.insert(overlay);
|
||||||
_mobileActionsOverlayEntry = overlay;
|
_mobileActionsOverlayEntry = overlay;
|
||||||
mobileActionsOverlayVisible.value = true;
|
setMobileActionsOverlayVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void hideMobileActionsOverlay() {
|
void hideMobileActionsOverlay({store = true}) {
|
||||||
if (_mobileActionsOverlayEntry != null) {
|
if (_mobileActionsOverlayEntry != null) {
|
||||||
_mobileActionsOverlayEntry!.remove();
|
_mobileActionsOverlayEntry!.remove();
|
||||||
_mobileActionsOverlayEntry = null;
|
_mobileActionsOverlayEntry = null;
|
||||||
mobileActionsOverlayVisible.value = false;
|
setMobileActionsOverlayVisible(false, store: store);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -891,30 +928,32 @@ class OverlayDialogManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
||||||
final position = SimpleWrapper(Offset(0, 0));
|
|
||||||
makeMobileActions(BuildContext context, double s) {
|
makeMobileActions(BuildContext context, double s) {
|
||||||
final scale = s < 0.85 ? 0.85 : s;
|
final scale = s < 0.85 ? 0.85 : s;
|
||||||
final session = ffi ?? gFFI;
|
final session = ffi ?? gFFI;
|
||||||
// compute overlay position
|
|
||||||
final screenW = MediaQuery.of(context).size.width;
|
|
||||||
final screenH = MediaQuery.of(context).size.height;
|
|
||||||
const double overlayW = 200;
|
const double overlayW = 200;
|
||||||
const double overlayH = 45;
|
const double overlayH = 45;
|
||||||
|
computeOverlayPosition() {
|
||||||
|
final screenW = MediaQuery.of(context).size.width;
|
||||||
|
final screenH = MediaQuery.of(context).size.height;
|
||||||
final left = (screenW - overlayW * scale) / 2;
|
final left = (screenW - overlayW * scale) / 2;
|
||||||
final top = screenH - (overlayH + 80) * scale;
|
final top = screenH - (overlayH + 80) * scale;
|
||||||
position.value = Offset(left, top);
|
return Offset(left, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draggablePositions.mobileActions.isInvalid()) {
|
||||||
|
draggablePositions.mobileActions.update(computeOverlayPosition());
|
||||||
|
} else {
|
||||||
|
draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale);
|
||||||
|
}
|
||||||
return DraggableMobileActions(
|
return DraggableMobileActions(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
position: position,
|
position: draggablePositions.mobileActions,
|
||||||
width: overlayW,
|
width: overlayW,
|
||||||
height: overlayH,
|
height: overlayH,
|
||||||
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
onBackPressed: session.inputModel.onMobileBack,
|
||||||
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
onHomePressed: session.inputModel.onMobileHome,
|
||||||
onRecentPressed: () async {
|
onRecentPressed: session.inputModel.onMobileApps,
|
||||||
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
|
||||||
},
|
|
||||||
onHidePressed: onHide,
|
onHidePressed: onHide,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1008,7 +1047,8 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
return KeyEventResult.handled; // avoid TextField exception on escape
|
return KeyEventResult.handled; // avoid TextField exception on escape
|
||||||
} else if (!tabTapped &&
|
} else if (!tabTapped &&
|
||||||
onSubmit != null &&
|
onSubmit != null &&
|
||||||
(key.logicalKey == LogicalKeyboardKey.enter || key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
(key.logicalKey == LogicalKeyboardKey.enter ||
|
||||||
|
key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
||||||
if (key is RawKeyDownEvent) onSubmit?.call();
|
if (key is RawKeyDownEvent) onSubmit?.call();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
@@ -1037,6 +1077,49 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget createDialogContent(String text) {
|
||||||
|
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
|
||||||
|
final List<TextSpan> spans = [];
|
||||||
|
int start = 0;
|
||||||
|
bool hasLink = false;
|
||||||
|
|
||||||
|
linkRegExp.allMatches(text).forEach((match) {
|
||||||
|
hasLink = true;
|
||||||
|
if (match.start > start) {
|
||||||
|
spans.add(TextSpan(text: text.substring(start, match.start)));
|
||||||
|
}
|
||||||
|
spans.add(TextSpan(
|
||||||
|
text: match.group(0) ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
String linkText = match.group(0) ?? '';
|
||||||
|
linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
|
||||||
|
launchUrl(Uri.parse(linkText));
|
||||||
|
},
|
||||||
|
));
|
||||||
|
start = match.end;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (start < text.length) {
|
||||||
|
spans.add(TextSpan(text: text.substring(start)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasLink) {
|
||||||
|
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectableText.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: TextStyle(color: Colors.black, fontSize: 15),
|
||||||
|
children: spans,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void msgBox(SessionID sessionId, String type, String title, String text,
|
void msgBox(SessionID sessionId, String type, String title, String text,
|
||||||
String link, OverlayDialogManager dialogManager,
|
String link, OverlayDialogManager dialogManager,
|
||||||
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
|
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
|
||||||
@@ -1045,7 +1128,7 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
bool hasOk = false;
|
bool hasOk = false;
|
||||||
submit() {
|
submit() {
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||||
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
|
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
|
||||||
closeConnection();
|
closeConnection();
|
||||||
}
|
}
|
||||||
@@ -1079,12 +1162,11 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (reconnect != null &&
|
if (reconnect != null && title == "Connection Error") {
|
||||||
title == "Connection Error" &&
|
|
||||||
reconnectTimeout != null) {
|
|
||||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||||
final enabled = true.obs;
|
final enabled = true.obs;
|
||||||
final button = Obx(() => _ReconnectCountDownButton(
|
final button = reconnectTimeout != null
|
||||||
|
? Obx(() => _ReconnectCountDownButton(
|
||||||
second: reconnectTimeout,
|
second: reconnectTimeout,
|
||||||
onPressed: enabled.isTrue
|
onPressed: enabled.isTrue
|
||||||
? () {
|
? () {
|
||||||
@@ -1093,7 +1175,20 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
reconnect(dialogManager, sessionId, false);
|
reconnect(dialogManager, sessionId, false);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
));
|
))
|
||||||
|
: Obx(
|
||||||
|
() => dialogButton(
|
||||||
|
'Reconnect',
|
||||||
|
isOutline: true,
|
||||||
|
onPressed: enabled.isTrue
|
||||||
|
? () {
|
||||||
|
// Disable the button
|
||||||
|
enabled.value = false;
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
buttons.insert(0, button);
|
buttons.insert(0, button);
|
||||||
}
|
}
|
||||||
if (link.isNotEmpty) {
|
if (link.isNotEmpty) {
|
||||||
@@ -1182,7 +1277,7 @@ Widget msgboxContent(String type, String title, String text) {
|
|||||||
translate(title),
|
translate(title),
|
||||||
style: TextStyle(fontSize: 21),
|
style: TextStyle(fontSize: 21),
|
||||||
).marginOnly(bottom: 10),
|
).marginOnly(bottom: 10),
|
||||||
Text(translateText(text), style: const TextStyle(fontSize: 15)),
|
createDialogContent(translateText(text)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1377,14 +1472,10 @@ class AndroidPermissionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move this to mobile/widgets.
|
|
||||||
// Used only for mobile, pages remote, settings, dialog
|
|
||||||
// TODO remove argument contentPadding, it’s not used, getToggle() has not
|
|
||||||
RadioListTile<T> getRadio<T>(
|
RadioListTile<T> getRadio<T>(
|
||||||
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
|
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
|
||||||
{EdgeInsetsGeometry? contentPadding, bool? dense}) {
|
{bool? dense}) {
|
||||||
return RadioListTile<T>(
|
return RadioListTile<T>(
|
||||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
controlAffinity: ListTileControlAffinity.trailing,
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -1411,7 +1502,7 @@ Future<void> initGlobalFFI() async {
|
|||||||
_globalFFI = FFI(null);
|
_globalFFI = FFI(null);
|
||||||
debugPrint("_globalFFI init end");
|
debugPrint("_globalFFI init end");
|
||||||
// after `put`, can also be globally found by Get.find<FFI>();
|
// after `put`, can also be globally found by Get.find<FFI>();
|
||||||
Get.put(_globalFFI, permanent: true);
|
Get.put<FFI>(_globalFFI, permanent: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
String translate(String name) {
|
String translate(String name) {
|
||||||
@@ -1421,14 +1512,16 @@ String translate(String name) {
|
|||||||
return platformFFI.translate(name, localeName);
|
return platformFFI.translate(name, localeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function must be kept the same as the one in rust and sciter code.
|
||||||
|
// rust: libs/hbb_common/src/config.rs -> option2bool()
|
||||||
|
// sciter: Does not have the function, but it should be kept the same.
|
||||||
bool option2bool(String option, String value) {
|
bool option2bool(String option, String value) {
|
||||||
bool res;
|
bool res;
|
||||||
if (option.startsWith("enable-")) {
|
if (option.startsWith("enable-")) {
|
||||||
res = value != "N";
|
res = value != "N";
|
||||||
} else if (option.startsWith("allow-") ||
|
} else if (option.startsWith("allow-") ||
|
||||||
option == "stop-service" ||
|
option == kOptionStopService ||
|
||||||
option == kOptionDirectServer ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = value == "Y";
|
res = value == "Y";
|
||||||
} else {
|
} else {
|
||||||
@@ -1443,9 +1536,8 @@ String bool2option(String option, bool b) {
|
|||||||
if (option.startsWith('enable-')) {
|
if (option.startsWith('enable-')) {
|
||||||
res = b ? defaultOptionYes : 'N';
|
res = b ? defaultOptionYes : 'N';
|
||||||
} else if (option.startsWith('allow-') ||
|
} else if (option.startsWith('allow-') ||
|
||||||
option == "stop-service" ||
|
option == kOptionStopService ||
|
||||||
option == kOptionDirectServer ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = b ? 'Y' : defaultOptionNo;
|
res = b ? 'Y' : defaultOptionNo;
|
||||||
} else {
|
} else {
|
||||||
@@ -1481,9 +1573,9 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
|
|||||||
return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
|
return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
|
||||||
}
|
}
|
||||||
|
|
||||||
mainSetPeerBoolOptionSync(String id, String key, bool v) {
|
// Don't use `option2bool()` and `bool2option()` to convert the session option.
|
||||||
bind.mainSetPeerOptionSync(id: id, key: key, value: bool2option(key, v));
|
// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
|
||||||
}
|
// Because all session options use `Y` and `<Empty>` as values.
|
||||||
|
|
||||||
Future<bool> matchPeer(String searchText, Peer peer) async {
|
Future<bool> matchPeer(String searchText, Peer peer) async {
|
||||||
if (searchText.isEmpty) {
|
if (searchText.isEmpty) {
|
||||||
@@ -2668,15 +2760,18 @@ Future<void> start_service(bool is_start) async {
|
|||||||
!isMacOS ||
|
!isMacOS ||
|
||||||
await callMainCheckSuperUserPermission();
|
await callMainCheckSuperUserPermission();
|
||||||
if (checked) {
|
if (checked) {
|
||||||
bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
|
mainSetBoolOption(kOptionStopService, !is_start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
Future<bool> canBeBlocked() async {
|
||||||
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
|
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
||||||
var block = false.obs;
|
var option = option2bool(kOptionAllowRemoteConfigModification,
|
||||||
return Obx(() => MouseRegion(
|
await bind.mainGetOption(key: kOptionAllowRemoteConfigModification));
|
||||||
onEnter: (_) async {
|
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
|
||||||
if (use != null && !await use()) {
|
if (use != null && !await use()) {
|
||||||
block.value = false;
|
block.value = false;
|
||||||
return;
|
return;
|
||||||
@@ -2687,12 +2782,28 @@ Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
|
|||||||
var d = time0 - await bind.mainGetMouseTime();
|
var d = time0 - await bind.mainGetMouseTime();
|
||||||
if (d < 120) {
|
if (d < 120) {
|
||||||
block.value = true;
|
block.value = true;
|
||||||
|
} else {
|
||||||
|
block.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
||||||
|
Widget buildRemoteBlock(
|
||||||
|
{required Widget child,
|
||||||
|
required RxBool block,
|
||||||
|
required bool mask,
|
||||||
|
WhetherUseRemoteBlock? use}) {
|
||||||
|
return Obx(() => MouseRegion(
|
||||||
|
onEnter: (_) async {
|
||||||
|
await shouldBeBlocked(block, use);
|
||||||
},
|
},
|
||||||
onExit: (event) => block.value = false,
|
onExit: (event) => block.value = false,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
child,
|
// scope block tab
|
||||||
|
FocusScope(child: child, canRequestFocus: !block.value),
|
||||||
|
// mask block click, cm not block click and still use check_click_time to avoid block local click
|
||||||
|
if (mask)
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !block.value,
|
offstage: !block.value,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -2745,12 +2856,10 @@ Widget buildErrorBanner(BuildContext context,
|
|||||||
required RxString err,
|
required RxString err,
|
||||||
required Function? retry,
|
required Function? retry,
|
||||||
required Function close}) {
|
required Function close}) {
|
||||||
const double height = 25;
|
|
||||||
return Obx(() => Offstage(
|
return Obx(() => Offstage(
|
||||||
offstage: !(!loading.value && err.value.isNotEmpty),
|
offstage: !(!loading.value && err.value.isNotEmpty),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: height,
|
|
||||||
color: MyTheme.color(context).errorBannerBg,
|
color: MyTheme.color(context).errorBannerBg,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -2767,9 +2876,8 @@ Widget buildErrorBanner(BuildContext context,
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: translate(err.value),
|
message: translate(err.value),
|
||||||
child: Text(
|
child: SelectableText(
|
||||||
translate(err.value),
|
translate(err.value),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
)).marginSymmetric(vertical: 2),
|
)).marginSymmetric(vertical: 2),
|
||||||
),
|
),
|
||||||
@@ -2889,6 +2997,16 @@ openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
|||||||
final displays = i == kAllDisplayValue
|
final displays = i == kAllDisplayValue
|
||||||
? List.generate(pi.displays.length, (index) => index)
|
? List.generate(pi.displays.length, (index) => index)
|
||||||
: [i];
|
: [i];
|
||||||
|
// Try clear image model before switching from all displays
|
||||||
|
// 1. The remote side has multiple displays.
|
||||||
|
// 2. Do not use texture render.
|
||||||
|
// 3. Connect to Display 1.
|
||||||
|
// 4. Switch to multi-displays `kAllDisplayValue`
|
||||||
|
// 5. Switch to Display 2.
|
||||||
|
// Then the remote page will display last picture of Display 1 at the beginning.
|
||||||
|
if (pi.forceTextureRender && i != kAllDisplayValue) {
|
||||||
|
ffi.imageModel.clearImage();
|
||||||
|
}
|
||||||
bind.sessionSwitchDisplay(
|
bind.sessionSwitchDisplay(
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
sessionId: ffi.sessionId,
|
sessionId: ffi.sessionId,
|
||||||
@@ -2922,10 +3040,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
|||||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewConnectWindowFrame(int windowId, String peerId, Rect? screenRect) async {
|
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
|
||||||
|
int? display, Rect? screenRect) async {
|
||||||
if (screenRect == null) {
|
if (screenRect == null) {
|
||||||
|
// Do not restore window position to new connection if there's a pre-session.
|
||||||
|
// https://github.com/rustdesk/rustdesk/discussions/8825
|
||||||
|
if (preSessionCount == 0) {
|
||||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||||
windowId: windowId, peerId: peerId);
|
windowId: windowId, display: display, peerId: peerId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await tryMoveToScreenAndSetFullscreen(screenRect);
|
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||||
}
|
}
|
||||||
@@ -3051,9 +3174,16 @@ Future<bool> setServerConfig(
|
|||||||
List<RxString>? errMsgs,
|
List<RxString>? errMsgs,
|
||||||
ServerConfig config,
|
ServerConfig config,
|
||||||
) async {
|
) async {
|
||||||
config.idServer = config.idServer.trim();
|
String removeEndSlash(String input) {
|
||||||
config.relayServer = config.relayServer.trim();
|
if (input.endsWith('/')) {
|
||||||
config.apiServer = config.apiServer.trim();
|
return input.substring(0, input.length - 1);
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.idServer = removeEndSlash(config.idServer.trim());
|
||||||
|
config.relayServer = removeEndSlash(config.relayServer.trim());
|
||||||
|
config.apiServer = removeEndSlash(config.apiServer.trim());
|
||||||
config.key = config.key.trim();
|
config.key = config.key.trim();
|
||||||
if (controllers != null) {
|
if (controllers != null) {
|
||||||
controllers[0].text = config.idServer;
|
controllers[0].text = config.idServer;
|
||||||
@@ -3262,16 +3392,11 @@ bool isInHomePage() {
|
|||||||
return controller.state.value.selected == 0;
|
return controller.state.value.selected == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildPresetPasswordWarning() {
|
Widget _buildPresetPasswordWarning() {
|
||||||
return FutureBuilder<bool>(
|
if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) !=
|
||||||
future: bind.isPresetPassword(),
|
'N') {
|
||||||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
return SizedBox.shrink();
|
||||||
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(
|
return Container(
|
||||||
color: Colors.yellow,
|
color: Colors.yellow,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -3293,6 +3418,27 @@ Widget buildPresetPasswordWarning() {
|
|||||||
],
|
],
|
||||||
).paddingAll(8),
|
).paddingAll(8),
|
||||||
); // Show a warning message if the Future completed with true
|
); // Show a warning message if the Future completed with true
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildPresetPasswordWarningMobile() {
|
||||||
|
if (bind.isPresetPasswordMobileOnly()) {
|
||||||
|
return _buildPresetPasswordWarning();
|
||||||
|
} else {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _buildPresetPasswordWarning();
|
||||||
} else {
|
} else {
|
||||||
return SizedBox
|
return SizedBox
|
||||||
.shrink(); // Show nothing if the Future completed with false or null
|
.shrink(); // Show nothing if the Future completed with false or null
|
||||||
@@ -3346,7 +3492,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
|
get windowResizeEdgeSize =>
|
||||||
|
isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
|
||||||
|
|
||||||
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
|
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
|
||||||
// See _linuxWindowResizable for more details.
|
// See _linuxWindowResizable for more details.
|
||||||
@@ -3364,7 +3511,12 @@ setResizable(bool resizable) {
|
|||||||
|
|
||||||
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
||||||
|
|
||||||
final isCustomClient = bind.isCustomClient();
|
bool? _isCustomClient;
|
||||||
|
bool get isCustomClient {
|
||||||
|
_isCustomClient ??= bind.isCustomClient();
|
||||||
|
return _isCustomClient!;
|
||||||
|
}
|
||||||
|
|
||||||
get defaultOptionLang => isCustomClient ? 'default' : '';
|
get defaultOptionLang => isCustomClient ? 'default' : '';
|
||||||
get defaultOptionTheme => isCustomClient ? 'system' : '';
|
get defaultOptionTheme => isCustomClient ? 'system' : '';
|
||||||
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
||||||
@@ -3372,3 +3524,66 @@ get defaultOptionNo => isCustomClient ? 'N' : '';
|
|||||||
get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
||||||
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
||||||
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
||||||
|
|
||||||
|
bool whitelistNotEmpty() {
|
||||||
|
// https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist
|
||||||
|
final v = bind.mainGetOptionSync(key: kOptionWhitelist);
|
||||||
|
return v != '' && v != ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
// `setMovable()` is only supported on macOS.
|
||||||
|
//
|
||||||
|
// On macOS, the window can be dragged by the tab bar by default.
|
||||||
|
// We need to disable the movable feature to prevent the window from being dragged by the tabs in the tab bar.
|
||||||
|
//
|
||||||
|
// When we drag the blank tab bar (not the tab), the window will be dragged normally by adding the `onPanStart` handle.
|
||||||
|
//
|
||||||
|
// See the following code for more details:
|
||||||
|
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L385
|
||||||
|
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L399
|
||||||
|
//
|
||||||
|
// @platforms macos
|
||||||
|
disableWindowMovable(int? windowId) {
|
||||||
|
if (!isMacOS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowId == null) {
|
||||||
|
windowManager.setMovable(false);
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(windowId).setMovable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget netWorkErrorWidget() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(translate("network_error_tip")),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: gFFI.userModel.refreshCurrentUser,
|
||||||
|
child: Text(translate("Retry")))
|
||||||
|
.marginSymmetric(vertical: 16),
|
||||||
|
SelectableText(gFFI.userModel.networkError.value,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.red)),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResizeEdge>? get windowManagerEnableResizeEdges => isWindows
|
||||||
|
? [
|
||||||
|
ResizeEdge.topLeft,
|
||||||
|
ResizeEdge.top,
|
||||||
|
ResizeEdge.topRight,
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
|
||||||
|
? [
|
||||||
|
SubWindowResizeEdge.topLeft,
|
||||||
|
SubWindowResizeEdge.top,
|
||||||
|
SubWindowResizeEdge.topRight,
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ class PrivacyModeState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString state = ''.obs;
|
final RxString state = ''.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxString>(state, tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = '';
|
Get.find<RxString>(tag: key).value = '';
|
||||||
}
|
}
|
||||||
@@ -33,9 +33,9 @@ class BlockInputState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,8 @@ class BlockInputState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,9 +56,9 @@ class CurrentDisplayState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = RxInt(0);
|
final RxInt state = RxInt(0);
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 0;
|
Get.find<RxInt>(tag: key).value = 0;
|
||||||
}
|
}
|
||||||
@@ -66,8 +66,8 @@ class CurrentDisplayState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,16 +105,16 @@ class ConnectionTypeState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<ConnectionType>(tag: key)) {
|
||||||
final ConnectionType collectionType = ConnectionType();
|
final ConnectionType collectionType = ConnectionType();
|
||||||
Get.put(collectionType, tag: key);
|
Get.put<ConnectionType>(collectionType, tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<ConnectionType>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<ConnectionType>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +127,9 @@ class FingerprintState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString state = ''.obs;
|
final RxString state = ''.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxString>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = '';
|
Get.find<RxString>(tag: key).value = '';
|
||||||
}
|
}
|
||||||
@@ -137,8 +137,8 @@ class FingerprintState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,9 +150,9 @@ class ShowRemoteCursorState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -160,8 +160,8 @@ class ShowRemoteCursorState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +173,9 @@ class ShowRemoteCursorLockState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -183,8 +183,8 @@ class ShowRemoteCursorLockState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,10 +196,10 @@ class KeyboardEnabledState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
// Server side, default true
|
// Server side, default true
|
||||||
final RxBool state = true.obs;
|
final RxBool state = true.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = true;
|
Get.find<RxBool>(tag: key).value = true;
|
||||||
}
|
}
|
||||||
@@ -207,8 +207,8 @@ class KeyboardEnabledState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,9 +220,9 @@ class RemoteCursorMovedState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -230,8 +230,8 @@ class RemoteCursorMovedState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +243,9 @@ class RemoteCountState {
|
|||||||
|
|
||||||
static void init() {
|
static void init() {
|
||||||
final key = tag();
|
final key = tag();
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = 1.obs;
|
final RxInt state = 1.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 1;
|
Get.find<RxInt>(tag: key).value = 1;
|
||||||
}
|
}
|
||||||
@@ -253,8 +253,8 @@ class RemoteCountState {
|
|||||||
|
|
||||||
static void delete() {
|
static void delete() {
|
||||||
final key = tag();
|
final key = tag();
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +266,9 @@ class PeerBoolOption {
|
|||||||
|
|
||||||
static void init(String id, String opt, bool Function() init_getter) {
|
static void init(String id, String opt, bool Function() init_getter) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool value = RxBool(init_getter());
|
final RxBool value = RxBool(init_getter());
|
||||||
Get.put(value, tag: key);
|
Get.put<RxBool>(value, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = init_getter();
|
Get.find<RxBool>(tag: key).value = init_getter();
|
||||||
}
|
}
|
||||||
@@ -276,8 +276,8 @@ class PeerBoolOption {
|
|||||||
|
|
||||||
static void delete(String id, String opt) {
|
static void delete(String id, String opt) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,9 +290,9 @@ class PeerStringOption {
|
|||||||
|
|
||||||
static void init(String id, String opt, String Function() init_getter) {
|
static void init(String id, String opt, String Function() init_getter) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString value = RxString(init_getter());
|
final RxString value = RxString(init_getter());
|
||||||
Get.put(value, tag: key);
|
Get.put<RxString>(value, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = init_getter();
|
Get.find<RxString>(tag: key).value = init_getter();
|
||||||
}
|
}
|
||||||
@@ -300,8 +300,8 @@ class PeerStringOption {
|
|||||||
|
|
||||||
static void delete(String id, String opt) {
|
static void delete(String id, String opt) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,9 +314,9 @@ class UnreadChatCountState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = RxInt(0);
|
final RxInt state = RxInt(0);
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 0;
|
Get.find<RxInt>(tag: key).value = 0;
|
||||||
}
|
}
|
||||||
@@ -324,8 +324,8 @@ class UnreadChatCountState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,17 +35,14 @@ class AddressBook extends StatefulWidget {
|
|||||||
class _AddressBookState extends State<AddressBook> {
|
class _AddressBookState extends State<AddressBook> {
|
||||||
var menuPos = RelativeRect.fill;
|
var menuPos = RelativeRect.fill;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Obx(() {
|
Widget build(BuildContext context) => Obx(() {
|
||||||
if (!gFFI.userModel.isLogin) {
|
if (!gFFI.userModel.isLogin) {
|
||||||
return Center(
|
return Center(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||||
|
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||||
|
return netWorkErrorWidget();
|
||||||
} else {
|
} else {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -110,6 +107,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAddressBookMobile() {
|
Widget _buildAddressBookMobile() {
|
||||||
|
const padding = 8.0;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -120,7 +118,8 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.background)),
|
color: Theme.of(context).colorScheme.background)),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding:
|
||||||
|
const EdgeInsets.fromLTRB(padding, 0, padding, padding),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -130,7 +129,6 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: _buildTags(),
|
child: _buildTags(),
|
||||||
),
|
),
|
||||||
_buildAbPermission(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -198,10 +196,9 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
if (contains) {
|
if (contains) {
|
||||||
names.insert(0, personalAddressBookName);
|
names.insert(0, personalAddressBookName);
|
||||||
}
|
}
|
||||||
final items = names
|
|
||||||
.map((e) => DropdownMenuItem(
|
Row buildItem(String e, {bool button = false}) {
|
||||||
value: e,
|
return Row(
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
@@ -209,13 +206,18 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
message: gFFI.abModel.translatedName(e),
|
message: gFFI.abModel.translatedName(e),
|
||||||
child: Text(
|
child: Text(
|
||||||
gFFI.abModel.translatedName(e),
|
gFFI.abModel.translatedName(e),
|
||||||
style: TextStyle(fontSize: 14.0),
|
style: button ? null : TextStyle(fontSize: 14.0),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: button ? TextAlign.center : null,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)))
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final items = names
|
||||||
|
.map((e) => DropdownMenuItem(value: e, child: buildItem(e)))
|
||||||
.toList();
|
.toList();
|
||||||
var menuItemStyleData = MenuItemStyleData(height: 36);
|
var menuItemStyleData = MenuItemStyleData(height: 36);
|
||||||
if (contains && items.length > 1) {
|
if (contains && items.length > 1) {
|
||||||
@@ -237,14 +239,22 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
customButton: Container(
|
||||||
|
height: isDesktop ? 48 : 40,
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||||
|
Icon(Icons.arrow_drop_down),
|
||||||
|
]),
|
||||||
|
),
|
||||||
underline: Container(
|
underline: Container(
|
||||||
height: 0.7,
|
height: 0.7,
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
),
|
),
|
||||||
buttonStyleData: ButtonStyleData(height: 48),
|
|
||||||
menuItemStyleData: menuItemStyleData,
|
menuItemStyleData: menuItemStyleData,
|
||||||
items: items,
|
items: items,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
|
isDense: true,
|
||||||
dropdownSearchData: DropdownSearchData(
|
dropdownSearchData: DropdownSearchData(
|
||||||
searchController: textEditingController,
|
searchController: textEditingController,
|
||||||
searchInnerWidgetHeight: 50,
|
searchInnerWidgetHeight: 50,
|
||||||
@@ -412,7 +422,8 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
|
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
|
||||||
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
||||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||||
sortMenuItem(),
|
if (gFFI.abModel.legacyMode.value)
|
||||||
|
sortMenuItem(), // It's already sorted after pulling down
|
||||||
if (canWrite) syncMenuItem(),
|
if (canWrite) syncMenuItem(),
|
||||||
filterMenuItem(),
|
filterMenuItem(),
|
||||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||||
|
|||||||
@@ -2,22 +2,39 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
|
||||||
|
const _kWindowsSystemSound = 'System Sound';
|
||||||
|
|
||||||
typedef AudioINputSetDevice = void Function(String device);
|
typedef AudioINputSetDevice = void Function(String device);
|
||||||
typedef AudioInputBuilder = Widget Function(
|
typedef AudioInputBuilder = Widget Function(
|
||||||
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
||||||
|
|
||||||
class AudioInput extends StatelessWidget {
|
class AudioInput extends StatelessWidget {
|
||||||
final AudioInputBuilder builder;
|
final AudioInputBuilder builder;
|
||||||
|
final bool isCm;
|
||||||
|
final bool isVoiceCall;
|
||||||
|
|
||||||
const AudioInput({Key? key, required this.builder}) : super(key: key);
|
const AudioInput(
|
||||||
|
{Key? key,
|
||||||
|
required this.builder,
|
||||||
|
required this.isCm,
|
||||||
|
required this.isVoiceCall})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
static String getDefault() {
|
static String getDefault() {
|
||||||
if (isWindows) return translate('System Sound');
|
if (isWindows) return translate('System Sound');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> getValue() async {
|
static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
|
||||||
String device = await bind.mainGetOption(key: 'audio-input');
|
if (isVoiceCall) {
|
||||||
|
return bind.getVoiceCallInputDevice(isCm: isCm);
|
||||||
|
} else {
|
||||||
|
return bind.mainGetOption(key: 'audio-input');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
|
||||||
|
String device = await getAudioInput(isCm, isVoiceCall);
|
||||||
if (device.isNotEmpty) {
|
if (device.isNotEmpty) {
|
||||||
return device;
|
return device;
|
||||||
} else {
|
} else {
|
||||||
@@ -25,31 +42,39 @@ class AudioInput extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setDevice(String device) async {
|
static Future<void> setDevice(
|
||||||
|
String device, bool isCm, bool isVoiceCall) async {
|
||||||
if (device == getDefault()) device = '';
|
if (device == getDefault()) device = '';
|
||||||
|
if (isVoiceCall) {
|
||||||
|
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
|
||||||
|
} else {
|
||||||
await bind.mainSetOption(key: 'audio-input', value: device);
|
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Map<String, Object>> getDevicesInfo() async {
|
static Future<Map<String, Object>> getDevicesInfo(
|
||||||
|
bool isCm, bool isVoiceCall) async {
|
||||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
devices.insert(0, translate('System Sound'));
|
devices.insert(0, translate(_kWindowsSystemSound));
|
||||||
}
|
}
|
||||||
String current = await getValue();
|
String current = await getValue(isCm, isVoiceCall);
|
||||||
return {'devices': devices, 'current': current};
|
return {'devices': devices, 'current': current};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return futureBuilder(
|
return futureBuilder(
|
||||||
future: getDevicesInfo(),
|
future: getDevicesInfo(isCm, isVoiceCall),
|
||||||
hasData: (data) {
|
hasData: (data) {
|
||||||
String currentDevice = data['current'];
|
String currentDevice = data['current'];
|
||||||
List<String> devices = data['devices'] as List<String>;
|
List<String> devices = data['devices'] as List<String>;
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
return builder(devices, currentDevice, setDevice);
|
return builder(devices, currentDevice, (devices) {
|
||||||
|
setDevice(devices, isCm, isVoiceCall);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
@@ -218,13 +219,15 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
if (!isOptFixed)dialogButton("Clear", onPressed: () async {
|
if (!isOptFixed)
|
||||||
|
dialogButton("Clear", onPressed: () async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||||
callback?.call();
|
callback?.call();
|
||||||
close();
|
close();
|
||||||
}, isOutline: true),
|
}, isOutline: true),
|
||||||
if (!isOptFixed) dialogButton(
|
if (!isOptFixed)
|
||||||
|
dialogButton(
|
||||||
"OK",
|
"OK",
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -236,7 +239,8 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
if (newWhiteListField.isEmpty) {
|
if (newWhiteListField.isEmpty) {
|
||||||
// pass
|
// pass
|
||||||
} else {
|
} else {
|
||||||
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
final ips =
|
||||||
|
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||||
// test ip
|
// test ip
|
||||||
final ipMatch = RegExp(
|
final ipMatch = RegExp(
|
||||||
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
||||||
@@ -675,6 +679,7 @@ class PasswordWidget extends StatefulWidget {
|
|||||||
this.reRequestFocus = false,
|
this.reRequestFocus = false,
|
||||||
this.hintText,
|
this.hintText,
|
||||||
this.errorText,
|
this.errorText,
|
||||||
|
this.title,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
@@ -682,6 +687,7 @@ class PasswordWidget extends StatefulWidget {
|
|||||||
final bool reRequestFocus;
|
final bool reRequestFocus;
|
||||||
final String? hintText;
|
final String? hintText;
|
||||||
final String? errorText;
|
final String? errorText;
|
||||||
|
final String? title;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PasswordWidget> createState() => _PasswordWidgetState();
|
State<PasswordWidget> createState() => _PasswordWidgetState();
|
||||||
@@ -725,7 +731,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DialogTextField(
|
return DialogTextField(
|
||||||
title: translate(DialogTextField.kPasswordTitle),
|
title: translate(widget.title ?? DialogTextField.kPasswordTitle),
|
||||||
hintText: translate(widget.hintText ?? 'Enter your password'),
|
hintText: translate(widget.hintText ?? 'Enter your password'),
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
prefixIcon: DialogTextField.kPasswordIcon,
|
prefixIcon: DialogTextField.kPasswordIcon,
|
||||||
@@ -1762,9 +1768,70 @@ void renameDialog(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void changeBot({Function()? callback}) async {
|
||||||
|
if (bind.mainHasValidBotSync()) {
|
||||||
|
await bind.mainSetOption(key: "bot", value: "");
|
||||||
|
callback?.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String errorText = '';
|
||||||
|
bool loading = false;
|
||||||
|
final controller = TextEditingController();
|
||||||
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
onVerify() async {
|
||||||
|
final token = controller.text.trim();
|
||||||
|
if (token == "") return;
|
||||||
|
loading = true;
|
||||||
|
errorText = '';
|
||||||
|
setState(() {});
|
||||||
|
final error = await bind.mainVerifyBot(token: token);
|
||||||
|
if (error == "") {
|
||||||
|
callback?.call();
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
errorText = translate(error);
|
||||||
|
loading = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final codeField = TextField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: translate('Token'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Telegram bot")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SelectableText(translate("enable-bot-desc"),
|
||||||
|
style: TextStyle(fontSize: 12))
|
||||||
|
.marginOnly(bottom: 12),
|
||||||
|
Row(children: [Expanded(child: codeField)]),
|
||||||
|
if (errorText != '')
|
||||||
|
Text(errorText, style: TextStyle(color: Colors.red))
|
||||||
|
.marginOnly(top: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
|
loading
|
||||||
|
? CircularProgressIndicator()
|
||||||
|
: dialogButton("OK", onPressed: onVerify),
|
||||||
|
],
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void change2fa({Function()? callback}) async {
|
void change2fa({Function()? callback}) async {
|
||||||
if (bind.mainHasValid2FaSync()) {
|
if (bind.mainHasValid2FaSync()) {
|
||||||
await bind.mainSetOption(key: "2fa", value: "");
|
await bind.mainSetOption(key: "2fa", value: "");
|
||||||
|
await bind.mainClearTrustedDevices();
|
||||||
callback?.call();
|
callback?.call();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1832,6 +1899,7 @@ void enter2FaDialog(
|
|||||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
final RxBool submitReady = false.obs;
|
final RxBool submitReady = false.obs;
|
||||||
|
final RxBool trustThisDevice = false.obs;
|
||||||
|
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
dialogManager.show((setState, close, context) {
|
dialogManager.show((setState, close, context) {
|
||||||
@@ -1841,7 +1909,7 @@ void enter2FaDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
gFFI.send2FA(sessionId, controller.text.trim());
|
gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
|
||||||
close();
|
close();
|
||||||
dialogManager.showLoading(translate('Logging in...'),
|
dialogManager.showLoading(translate('Logging in...'),
|
||||||
onCancel: closeConnection);
|
onCancel: closeConnection);
|
||||||
@@ -1855,9 +1923,27 @@ void enter2FaDialog(
|
|||||||
onChanged: () => submitReady.value = codeField.isReady,
|
onChanged: () => submitReady.value = codeField.isReady,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final trustField = Obx(() => CheckboxListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
title: Text(translate("Trust this device")),
|
||||||
|
value: trustThisDevice.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
trustThisDevice.value = value;
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('enter-2fa-title')),
|
title: Text(translate('enter-2fa-title')),
|
||||||
content: codeField,
|
content: Column(
|
||||||
|
children: [
|
||||||
|
codeField,
|
||||||
|
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
|
||||||
|
trustField,
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton('Cancel',
|
dialogButton('Cancel',
|
||||||
onPressed: cancel,
|
onPressed: cancel,
|
||||||
@@ -2124,3 +2210,280 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
|
||||||
|
VoidCallback onConfirm) {
|
||||||
|
dialogManager.show((setState, close, context) {
|
||||||
|
submit() {
|
||||||
|
close();
|
||||||
|
onConfirm.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(content,
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
textAlign: TextAlign.start),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).marginOnly(bottom: 12),
|
||||||
|
actions: [
|
||||||
|
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||||
|
dialogButton(translate("OK"), onPressed: submit),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeUnlockPinDialog(String oldPin, Function() callback) {
|
||||||
|
final pinController = TextEditingController(text: oldPin);
|
||||||
|
final confirmController = TextEditingController(text: oldPin);
|
||||||
|
String? pinErrorText;
|
||||||
|
String? confirmationErrorText;
|
||||||
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
submit() async {
|
||||||
|
pinErrorText = null;
|
||||||
|
confirmationErrorText = null;
|
||||||
|
final pin = pinController.text.trim();
|
||||||
|
final confirm = confirmController.text.trim();
|
||||||
|
if (pin != confirm) {
|
||||||
|
setState(() {
|
||||||
|
confirmationErrorText =
|
||||||
|
translate('The confirmation is not identical.');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final errorMsg = bind.mainSetUnlockPin(pin: pin);
|
||||||
|
if (errorMsg != '') {
|
||||||
|
setState(() {
|
||||||
|
pinErrorText = translate(errorMsg);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback.call();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Set PIN")),
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
DialogTextField(
|
||||||
|
title: 'PIN',
|
||||||
|
controller: pinController,
|
||||||
|
obscureText: true,
|
||||||
|
errorText: pinErrorText,
|
||||||
|
),
|
||||||
|
DialogTextField(
|
||||||
|
title: translate('Confirmation'),
|
||||||
|
controller: confirmController,
|
||||||
|
obscureText: true,
|
||||||
|
errorText: confirmationErrorText,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).marginOnly(bottom: 12),
|
||||||
|
actions: [
|
||||||
|
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||||
|
dialogButton(translate("OK"), onPressed: submit),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkUnlockPinDialog(String correctPin, Function() passCallback) {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
String? errorText;
|
||||||
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
submit() async {
|
||||||
|
final pin = controller.text.trim();
|
||||||
|
if (correctPin != pin) {
|
||||||
|
setState(() {
|
||||||
|
errorText = translate('Wrong PIN');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
passCallback.call();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: PasswordWidget(
|
||||||
|
title: 'PIN',
|
||||||
|
controller: controller,
|
||||||
|
errorText: errorText,
|
||||||
|
hintText: '',
|
||||||
|
))
|
||||||
|
],
|
||||||
|
).marginOnly(bottom: 12),
|
||||||
|
actions: [
|
||||||
|
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||||
|
dialogButton(translate("OK"), onPressed: submit),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void confrimDeleteTrustedDevicesDialog(
|
||||||
|
RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
|
||||||
|
CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
|
||||||
|
() async {
|
||||||
|
if (selectedDevices.isEmpty) return;
|
||||||
|
if (selectedDevices.length == trustedDevices.length) {
|
||||||
|
await bind.mainClearTrustedDevices();
|
||||||
|
trustedDevices.clear();
|
||||||
|
selectedDevices.clear();
|
||||||
|
} else {
|
||||||
|
final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
|
||||||
|
await bind.mainRemoveTrustedDevices(json: json);
|
||||||
|
trustedDevices.removeWhere((element) {
|
||||||
|
return selectedDevices.contains(element.hwid);
|
||||||
|
});
|
||||||
|
selectedDevices.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void manageTrustedDeviceDialog() async {
|
||||||
|
RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
|
||||||
|
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||||
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Manage trusted devices")),
|
||||||
|
content: trustedDevicesTable(trustedDevices, selectedDevices),
|
||||||
|
actions: [
|
||||||
|
Obx(() => dialogButton(translate("Delete"),
|
||||||
|
onPressed: selectedDevices.isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
confrimDeleteTrustedDevicesDialog(
|
||||||
|
trustedDevices,
|
||||||
|
selectedDevices,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isOutline: false)
|
||||||
|
.marginOnly(top: 12)),
|
||||||
|
dialogButton(translate("Close"), onPressed: close, isOutline: true)
|
||||||
|
.marginOnly(top: 12),
|
||||||
|
],
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrustedDevice {
|
||||||
|
late final Uint8List hwid;
|
||||||
|
late final int time;
|
||||||
|
late final String id;
|
||||||
|
late final String name;
|
||||||
|
late final String platform;
|
||||||
|
|
||||||
|
TrustedDevice.fromJson(Map<String, dynamic> json) {
|
||||||
|
final hwidList = json['hwid'] as List<dynamic>;
|
||||||
|
hwid = Uint8List.fromList(hwidList.cast<int>());
|
||||||
|
time = json['time'];
|
||||||
|
id = json['id'];
|
||||||
|
name = json['name'];
|
||||||
|
platform = json['platform'];
|
||||||
|
}
|
||||||
|
|
||||||
|
String daysRemaining() {
|
||||||
|
final expiry = time + 90 * 24 * 60 * 60 * 1000;
|
||||||
|
final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
|
||||||
|
if (remaining < 0) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<TrustedDevice>> get() async {
|
||||||
|
final List<TrustedDevice> devices = List.empty(growable: true);
|
||||||
|
try {
|
||||||
|
final devicesJson = await bind.mainGetTrustedDevices();
|
||||||
|
if (devicesJson.isNotEmpty) {
|
||||||
|
final devicesList = json.decode(devicesJson);
|
||||||
|
if (devicesList is List) {
|
||||||
|
for (var device in devicesList) {
|
||||||
|
devices.add(TrustedDevice.fromJson(device));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(e.toString());
|
||||||
|
}
|
||||||
|
devices.sort((a, b) => b.time.compareTo(a.time));
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget trustedDevicesTable(
|
||||||
|
RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
|
||||||
|
RxBool selectAll = false.obs;
|
||||||
|
setSelectAll() {
|
||||||
|
if (selectedDevices.isNotEmpty &&
|
||||||
|
selectedDevices.length == devices.length) {
|
||||||
|
selectAll.value = true;
|
||||||
|
} else {
|
||||||
|
selectAll.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.listen((_) {
|
||||||
|
setSelectAll();
|
||||||
|
});
|
||||||
|
selectedDevices.listen((_) {
|
||||||
|
setSelectAll();
|
||||||
|
});
|
||||||
|
return FittedBox(
|
||||||
|
child: Obx(() => DataTable(
|
||||||
|
columns: [
|
||||||
|
DataColumn(
|
||||||
|
label: Checkbox(
|
||||||
|
value: selectAll.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == true) {
|
||||||
|
selectedDevices.clear();
|
||||||
|
selectedDevices.addAll(devices.map((e) => e.hwid));
|
||||||
|
} else {
|
||||||
|
selectedDevices.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
DataColumn(label: Text(translate('Platform'))),
|
||||||
|
DataColumn(label: Text(translate('ID'))),
|
||||||
|
DataColumn(label: Text(translate('Username'))),
|
||||||
|
DataColumn(label: Text(translate('Days remaining'))),
|
||||||
|
],
|
||||||
|
rows: devices.map((device) {
|
||||||
|
return DataRow(cells: [
|
||||||
|
DataCell(Checkbox(
|
||||||
|
value: selectedDevices.contains(device.hwid),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
if (value) {
|
||||||
|
selectedDevices.remove(device.hwid);
|
||||||
|
selectedDevices.add(device.hwid);
|
||||||
|
} else {
|
||||||
|
selectedDevices.remove(device.hwid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
DataCell(Text(device.platform)),
|
||||||
|
DataCell(Text(device.id)),
|
||||||
|
DataCell(Text(device.name)),
|
||||||
|
DataCell(Text(device.daysRemaining())),
|
||||||
|
]);
|
||||||
|
}).toList(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: This debounce logic is not working properly.
|
||||||
|
// If we move our finger very fast, we won't be able to detect the "oneFingerPan" event sometimes.
|
||||||
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
||||||
start(ScaleUpdateDetails d) {
|
start(ScaleUpdateDetails d) {
|
||||||
_currentState = GestureState.oneFingerPan;
|
_currentState = GestureState.oneFingerPan;
|
||||||
|
|||||||
@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
|
|||||||
String _failedMsg = '';
|
String _failedMsg = '';
|
||||||
String _url = '';
|
String _url = '';
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -455,7 +450,7 @@ Future<bool?> loginDialog() async {
|
|||||||
}
|
}
|
||||||
if (isEmailVerification != null) {
|
if (isEmailVerification != null) {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
if (close != null) close(false);
|
if (close != null) close(null);
|
||||||
verificationCodeDialog(
|
verificationCodeDialog(
|
||||||
resp.user, resp.secret, isEmailVerification);
|
resp.user, resp.secret, isEmailVerification);
|
||||||
} else {
|
} else {
|
||||||
@@ -712,6 +707,11 @@ Future<bool?> verificationCodeDialog(
|
|||||||
dialogButton("Verify", onPressed: getOnSubmit()),
|
dialogButton("Verify", onPressed: getOnSubmit()),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
// For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
|
||||||
|
// otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
|
||||||
|
if (isMobile && res == true) {
|
||||||
|
await UserModel.updateOtherModels();
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,6 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
RxString get searchUserText => gFFI.groupModel.searchUserText;
|
RxString get searchUserText => gFFI.groupModel.searchUserText;
|
||||||
static TextEditingController searchUserController = TextEditingController();
|
static TextEditingController searchUserController = TextEditingController();
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
@@ -35,6 +30,8 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
return Center(
|
return Center(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||||
|
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||||
|
return netWorkErrorWidget();
|
||||||
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
|
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (draggablePositions.chatWindow.isInvalid()) {
|
||||||
|
draggablePositions.chatWindow.update(position);
|
||||||
|
}
|
||||||
return isIOS
|
return isIOS
|
||||||
? IOSDraggable(
|
? IOSDraggable(
|
||||||
position: position,
|
position: draggablePositions.chatWindow,
|
||||||
chatModel: chatModel,
|
chatModel: chatModel,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: Draggable(
|
: Draggable(
|
||||||
checkKeyboard: true,
|
checkKeyboard: true,
|
||||||
position: SimpleWrapper(position),
|
position: draggablePositions.chatWindow,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
chatModel: chatModel,
|
chatModel: chatModel,
|
||||||
@@ -176,7 +181,7 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
required this.scale});
|
required this.scale});
|
||||||
|
|
||||||
final double scale;
|
final double scale;
|
||||||
final SimpleWrapper<Offset> position;
|
final DraggableKeyPosition position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final VoidCallback? onBackPressed;
|
final VoidCallback? onBackPressed;
|
||||||
@@ -241,6 +246,92 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DraggableKeyPosition {
|
||||||
|
final String key;
|
||||||
|
Offset _pos;
|
||||||
|
late Debouncer<int> _debouncerStore;
|
||||||
|
DraggableKeyPosition(this.key)
|
||||||
|
: _pos = DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
|
||||||
|
get pos => _pos;
|
||||||
|
|
||||||
|
_loadPosition(String k) {
|
||||||
|
final value = bind.getLocalFlutterOption(k: k);
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
final parts = value.split(',');
|
||||||
|
if (parts.length == 2) {
|
||||||
|
return Offset(double.parse(parts[0]), double.parse(parts[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
_pos = _loadPosition(key);
|
||||||
|
_debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
|
||||||
|
onChanged: (v) => _store(), initialValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(Offset pos) {
|
||||||
|
_pos = pos;
|
||||||
|
_triggerStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust position to keep it in the screen
|
||||||
|
// Only used for desktop and web desktop
|
||||||
|
tryAdjust(double w, double h, double scale) {
|
||||||
|
final size = MediaQuery.of(Get.context!).size;
|
||||||
|
w = w * scale;
|
||||||
|
h = h * scale;
|
||||||
|
double x = _pos.dx;
|
||||||
|
double y = _pos.dy;
|
||||||
|
if (x + w > size.width) {
|
||||||
|
x = size.width - w;
|
||||||
|
}
|
||||||
|
final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
|
||||||
|
if (y + h > (size.height - tabBarHeight)) {
|
||||||
|
y = size.height - tabBarHeight - h;
|
||||||
|
}
|
||||||
|
if (x < 0) {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
if (y < 0) {
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
if (x != _pos.dx || y != _pos.dy) {
|
||||||
|
update(Offset(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isInvalid() {
|
||||||
|
return _pos == DraggablePositions.kInvalidDraggablePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
|
||||||
|
_store() {
|
||||||
|
bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DraggablePositions {
|
||||||
|
static const kChatWindow = 'draggablePositionChat';
|
||||||
|
static const kMobileActions = 'draggablePositionMobile';
|
||||||
|
static const kIOSDraggable = 'draggablePositionIOS';
|
||||||
|
|
||||||
|
static const kInvalidDraggablePosition = Offset(-999999, -999999);
|
||||||
|
final chatWindow = DraggableKeyPosition(kChatWindow);
|
||||||
|
final mobileActions = DraggableKeyPosition(kMobileActions);
|
||||||
|
final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
|
||||||
|
|
||||||
|
load() {
|
||||||
|
chatWindow.load();
|
||||||
|
mobileActions.load();
|
||||||
|
iOSDraggable.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DraggablePositions draggablePositions = DraggablePositions();
|
||||||
|
|
||||||
class Draggable extends StatefulWidget {
|
class Draggable extends StatefulWidget {
|
||||||
Draggable(
|
Draggable(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
@@ -255,14 +346,14 @@ class Draggable extends StatefulWidget {
|
|||||||
|
|
||||||
final bool checkKeyboard;
|
final bool checkKeyboard;
|
||||||
final bool checkScreenSize;
|
final bool checkScreenSize;
|
||||||
final SimpleWrapper<Offset> position;
|
final DraggableKeyPosition position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final ChatModel? chatModel;
|
final ChatModel? chatModel;
|
||||||
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _DraggableState();
|
State<StatefulWidget> createState() => _DraggableState(chatModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableState extends State<Draggable> {
|
class _DraggableState extends State<Draggable> {
|
||||||
@@ -271,13 +362,11 @@ class _DraggableState extends State<Draggable> {
|
|||||||
double _saveHeight = 0;
|
double _saveHeight = 0;
|
||||||
double _lastBottomHeight = 0;
|
double _lastBottomHeight = 0;
|
||||||
|
|
||||||
@override
|
_DraggableState(ChatModel? chatModel) {
|
||||||
void initState() {
|
_chatModel = chatModel;
|
||||||
super.initState();
|
|
||||||
_chatModel = widget.chatModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get position => widget.position.value;
|
get position => widget.position.pos;
|
||||||
|
|
||||||
void onPanUpdate(DragUpdateDetails d) {
|
void onPanUpdate(DragUpdateDetails d) {
|
||||||
final offset = d.delta;
|
final offset = d.delta;
|
||||||
@@ -301,7 +390,7 @@ class _DraggableState extends State<Draggable> {
|
|||||||
y = position.dy + offset.dy;
|
y = position.dy + offset.dy;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.position.value = Offset(x, y);
|
widget.position.update(Offset(x, y));
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(position);
|
_chatModel?.setChatWindowPosition(position);
|
||||||
}
|
}
|
||||||
@@ -320,7 +409,7 @@ class _DraggableState extends State<Draggable> {
|
|||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.position.value = Offset(position.dx, _saveHeight);
|
widget.position.update(Offset(position.dx, _saveHeight));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +420,7 @@ class _DraggableState extends State<Draggable> {
|
|||||||
if (sumHeight + position.dy > contextHeight) {
|
if (sumHeight + position.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
widget.position.value = Offset(position.dx, y);
|
widget.position.update(Offset(position.dx, y));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,25 +451,25 @@ class _DraggableState extends State<Draggable> {
|
|||||||
class IOSDraggable extends StatefulWidget {
|
class IOSDraggable extends StatefulWidget {
|
||||||
const IOSDraggable(
|
const IOSDraggable(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
this.position = Offset.zero,
|
|
||||||
this.chatModel,
|
this.chatModel,
|
||||||
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
required this.builder})
|
required this.builder})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final Offset position;
|
final DraggableKeyPosition position;
|
||||||
final ChatModel? chatModel;
|
final ChatModel? chatModel;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final Widget Function(BuildContext) builder;
|
final Widget Function(BuildContext) builder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
IOSDraggableState createState() => IOSDraggableState();
|
IOSDraggableState createState() =>
|
||||||
|
IOSDraggableState(chatModel, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
class IOSDraggableState extends State<IOSDraggable> {
|
class IOSDraggableState extends State<IOSDraggable> {
|
||||||
late Offset _position;
|
|
||||||
late ChatModel? _chatModel;
|
late ChatModel? _chatModel;
|
||||||
late double _width;
|
late double _width;
|
||||||
late double _height;
|
late double _height;
|
||||||
@@ -388,28 +477,27 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
double _saveHeight = 0;
|
double _saveHeight = 0;
|
||||||
double _lastBottomHeight = 0;
|
double _lastBottomHeight = 0;
|
||||||
|
|
||||||
@override
|
IOSDraggableState(ChatModel? chatModel, double w, double h) {
|
||||||
void initState() {
|
_chatModel = chatModel;
|
||||||
super.initState();
|
_width = w;
|
||||||
_position = widget.position;
|
_height = h;
|
||||||
_chatModel = widget.chatModel;
|
|
||||||
_width = widget.width;
|
|
||||||
_height = widget.height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DraggableKeyPosition get position => widget.position;
|
||||||
|
|
||||||
checkKeyboard() {
|
checkKeyboard() {
|
||||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||||
final currentVisible = bottomHeight != 0;
|
final currentVisible = bottomHeight != 0;
|
||||||
|
|
||||||
// save
|
// save
|
||||||
if (!_keyboardVisible && currentVisible) {
|
if (!_keyboardVisible && currentVisible) {
|
||||||
_saveHeight = _position.dy;
|
_saveHeight = position.pos.dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, _saveHeight);
|
position.update(Offset(position.pos.dx, _saveHeight));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,10 +505,10 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
if (_keyboardVisible && currentVisible) {
|
if (_keyboardVisible && currentVisible) {
|
||||||
final sumHeight = bottomHeight + _height;
|
final sumHeight = bottomHeight + _height;
|
||||||
final contextHeight = MediaQuery.of(context).size.height;
|
final contextHeight = MediaQuery.of(context).size.height;
|
||||||
if (sumHeight + _position.dy > contextHeight) {
|
if (sumHeight + position.pos.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, y);
|
position.update(Offset(position.pos.dx, y));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,14 +523,14 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
left: _position.dx,
|
left: position.pos.dx,
|
||||||
top: _position.dy,
|
top: position.pos.dy,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onPanUpdate: (details) {
|
onPanUpdate: (details) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position += details.delta;
|
position.update(position.pos + details.delta);
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(_position);
|
_chatModel?.setChatWindowPosition(position.pos);
|
||||||
},
|
},
|
||||||
child: Material(
|
child: Material(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -498,8 +586,10 @@ class QualityMonitor extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
|
_row("Speed", qualityMonitorModel.data.speed ?? '-'),
|
||||||
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
|
_row("FPS", qualityMonitorModel.data.fps ?? '-'),
|
||||||
|
// let delay be 0 if fps is 0
|
||||||
_row(
|
_row(
|
||||||
"Delay", "${qualityMonitorModel.data.delay ?? '-'}ms",
|
"Delay",
|
||||||
|
"${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
|
||||||
rightColor: Colors.green),
|
rightColor: Colors.green),
|
||||||
_row("Target Bitrate",
|
_row("Target Bitrate",
|
||||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ enum PeerUiType { grid, tile, list }
|
|||||||
|
|
||||||
final peerCardUiType = PeerUiType.grid.obs;
|
final peerCardUiType = PeerUiType.grid.obs;
|
||||||
|
|
||||||
|
bool? hideUsernameOnCard;
|
||||||
|
|
||||||
class _PeerCard extends StatefulWidget {
|
class _PeerCard extends StatefulWidget {
|
||||||
final Peer peer;
|
final Peer peer;
|
||||||
final PeerTabIndex tab;
|
final PeerTabIndex tab;
|
||||||
@@ -130,8 +132,11 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
|
|
||||||
Widget _buildPeerTile(
|
Widget _buildPeerTile(
|
||||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||||
final name =
|
hideUsernameOnCard ??=
|
||||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||||
|
final name = hideUsernameOnCard == true
|
||||||
|
? peer.hostname
|
||||||
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||||
final greyStyle = TextStyle(
|
final greyStyle = TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||||
@@ -239,8 +244,11 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
|
|
||||||
Widget _buildPeerCard(
|
Widget _buildPeerCard(
|
||||||
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
||||||
final name =
|
hideUsernameOnCard ??=
|
||||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||||
|
final name = hideUsernameOnCard == true
|
||||||
|
? peer.hostname
|
||||||
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||||
final child = Card(
|
final child = Card(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -628,8 +636,8 @@ abstract class BasePeerCard extends StatelessWidget {
|
|||||||
|
|
||||||
@protected
|
@protected
|
||||||
Future<bool> _isForceAlwaysRelay(String id) async {
|
Future<bool> _isForceAlwaysRelay(String id) async {
|
||||||
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))
|
return option2bool(kOptionForceAlwaysRelay,
|
||||||
.isNotEmpty;
|
(await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
@@ -887,7 +895,7 @@ class RecentPeerCard extends BasePeerCard {
|
|||||||
menuItems.add(_createShortCutAction(peer.id));
|
menuItems.add(_createShortCutAction(peer.id));
|
||||||
}
|
}
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||||
@@ -943,7 +951,7 @@ class FavoritePeerCard extends BasePeerCard {
|
|||||||
menuItems.add(_createShortCutAction(peer.id));
|
menuItems.add(_createShortCutAction(peer.id));
|
||||||
}
|
}
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
if (await bind.mainPeerHasPassword(id: peer.id)) {
|
||||||
@@ -1048,7 +1056,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
}
|
}
|
||||||
if (gFFI.abModel.current.canWrite()) {
|
if (gFFI.abModel.current.canWrite()) {
|
||||||
menuItems.add(MenuEntryDivider());
|
menuItems.add(MenuEntryDivider());
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isMobile || isDesktop || isWebDesktop) {
|
||||||
menuItems.add(_renameAction(peer.id));
|
menuItems.add(_renameAction(peer.id));
|
||||||
}
|
}
|
||||||
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {
|
||||||
|
|||||||
@@ -76,8 +76,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
|
|
||||||
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||||
|
|
||||||
@override
|
_PeerTabPageState() {
|
||||||
void initState() {
|
_loadLocalOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadLocalOptions() {
|
||||||
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||||
if (uiType != '') {
|
if (uiType != '') {
|
||||||
peerCardUiType.value = int.parse(uiType) == 0
|
peerCardUiType.value = int.parse(uiType) == 0
|
||||||
@@ -87,8 +90,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
: PeerUiType.list;
|
: PeerUiType.list;
|
||||||
}
|
}
|
||||||
hideAbTagsPanel.value =
|
hideAbTagsPanel.value =
|
||||||
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
|
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
|
||||||
super.initState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleTabSelection(int tabIndex) async {
|
Future<void> handleTabSelection(int tabIndex) async {
|
||||||
@@ -399,9 +401,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
final peers = model.selectedPeers;
|
final peers = model.selectedPeers;
|
||||||
switch (model.currentTab) {
|
switch (model.currentTab) {
|
||||||
case 0:
|
case 0:
|
||||||
peers.map((p) async {
|
for (var p in peers) {
|
||||||
await bind.mainRemovePeer(id: p.id);
|
await bind.mainRemovePeer(id: p.id);
|
||||||
}).toList();
|
}
|
||||||
await bind.mainLoadRecentPeers();
|
await bind.mainLoadRecentPeers();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
@@ -413,9 +415,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
await bind.mainLoadFavPeers();
|
await bind.mainLoadFavPeers();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
peers.map((p) async {
|
for (var p in peers) {
|
||||||
await bind.mainRemoveDiscovered(id: p.id);
|
await bind.mainRemoveDiscovered(id: p.id);
|
||||||
}).toList();
|
}
|
||||||
await bind.mainLoadLanPeers();
|
await bind.mainLoadLanPeers();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
@@ -802,13 +804,20 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 36,
|
height: 36,
|
||||||
child: getRadio<PeerUiType>(
|
child: getRadio<PeerUiType>(
|
||||||
Text(
|
Tooltip(
|
||||||
translate(types.indexOf(e) == 0
|
message: translate(types.indexOf(e) == 0
|
||||||
? 'Big tiles'
|
? 'Big tiles'
|
||||||
: types.indexOf(e) == 1
|
: types.indexOf(e) == 1
|
||||||
? 'Small tiles'
|
? 'Small tiles'
|
||||||
: 'List'),
|
: 'List'),
|
||||||
style: style),
|
child: Icon(
|
||||||
|
e == PeerUiType.grid
|
||||||
|
? Icons.grid_view_rounded
|
||||||
|
: e == PeerUiType.list
|
||||||
|
? Icons.view_list_rounded
|
||||||
|
: Icons.view_agenda_rounded,
|
||||||
|
size: 18,
|
||||||
|
)),
|
||||||
e,
|
e,
|
||||||
peerCardUiType.value,
|
peerCardUiType.value,
|
||||||
dense: true,
|
dense: true,
|
||||||
@@ -838,7 +847,7 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
peerCardUiType.value == PeerUiType.grid
|
peerCardUiType.value == PeerUiType.grid
|
||||||
? Icons.grid_view_rounded
|
? Icons.grid_view_rounded
|
||||||
: peerCardUiType.value == PeerUiType.tile
|
: peerCardUiType.value == PeerUiType.list
|
||||||
? Icons.view_list_rounded
|
? Icons.view_list_rounded
|
||||||
: Icons.view_agenda_rounded,
|
: Icons.view_agenda_rounded,
|
||||||
size: 18,
|
size: 18,
|
||||||
@@ -865,17 +874,19 @@ class PeerSortDropdown extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||||
@override
|
_PeerSortDropdownState() {
|
||||||
void initState() {
|
|
||||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||||
|
_loadLocalOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadLocalOptions() {
|
||||||
peerSort.value = PeerSortType.remoteId;
|
peerSort.value = PeerSortType.remoteId;
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kOptionPeerSorting,
|
k: kOptionPeerSorting,
|
||||||
v: peerSort.value,
|
v: peerSort.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -45,10 +45,14 @@ class LoadEvent {
|
|||||||
final peerSearchText = "".obs;
|
final peerSearchText = "".obs;
|
||||||
|
|
||||||
/// for peer sort, global obs value
|
/// for peer sort, global obs value
|
||||||
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
RxString? _peerSort;
|
||||||
|
RxString get peerSort {
|
||||||
|
_peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||||
|
return _peerSort!;
|
||||||
|
}
|
||||||
|
|
||||||
// list for listener
|
// list for listener
|
||||||
final obslist = [peerSearchText, peerSort].obs;
|
RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
|
||||||
|
|
||||||
final peerSearchTextController =
|
final peerSearchTextController =
|
||||||
TextEditingController(text: peerSearchText.value);
|
TextEditingController(text: peerSearchText.value);
|
||||||
@@ -70,7 +74,8 @@ class _PeersView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// State for the peer widget.
|
/// State for the peer widget.
|
||||||
class _PeersViewState extends State<_PeersView> with WindowListener {
|
class _PeersViewState extends State<_PeersView>
|
||||||
|
with WindowListener, WidgetsBindingObserver {
|
||||||
static const int _maxQueryCount = 3;
|
static const int _maxQueryCount = 3;
|
||||||
final HashMap<String, String> _emptyMessages = HashMap.from({
|
final HashMap<String, String> _emptyMessages = HashMap.from({
|
||||||
LoadEvent.recent: 'empty_recent_tip',
|
LoadEvent.recent: 'empty_recent_tip',
|
||||||
@@ -82,9 +87,10 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
final _curPeers = <String>{};
|
final _curPeers = <String>{};
|
||||||
var _lastChangeTime = DateTime.now();
|
var _lastChangeTime = DateTime.now();
|
||||||
var _lastQueryPeers = <String>{};
|
var _lastQueryPeers = <String>{};
|
||||||
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
|
var _lastQueryTime = DateTime.now();
|
||||||
var _queryCount = 0;
|
var _queryCount = 0;
|
||||||
var _exit = false;
|
var _exit = false;
|
||||||
|
bool _isActive = true;
|
||||||
|
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
@@ -95,12 +101,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
windowManager.removeListener(this);
|
windowManager.removeListener(this);
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_exit = true;
|
_exit = true;
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -115,6 +123,20 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
_queryCount = _maxQueryCount;
|
_queryCount = _maxQueryCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function is required for mobile.
|
||||||
|
// `onWindowFocus` works fine for desktop.
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (isDesktop) return;
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
_isActive = true;
|
||||||
|
_queryCount = 0;
|
||||||
|
} else if (state == AppLifecycleState.inactive) {
|
||||||
|
_isActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<Peers>(
|
return ChangeNotifierProvider<Peers>(
|
||||||
@@ -253,10 +275,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
final _queryInterval = const Duration(seconds: 20);
|
var _queryInterval = const Duration(seconds: 20);
|
||||||
|
|
||||||
void _startCheckOnlines() {
|
void _startCheckOnlines() {
|
||||||
() async {
|
() async {
|
||||||
|
final p = await bind.mainIsUsingPublicServer();
|
||||||
|
if (!p) {
|
||||||
|
_queryInterval = const Duration(seconds: 6);
|
||||||
|
}
|
||||||
while (!_exit) {
|
while (!_exit) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
||||||
@@ -264,7 +290,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
_queryOnlines(false);
|
_queryOnlines(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_queryCount < _maxQueryCount) {
|
if (_isActive && (_queryCount < _maxQueryCount || !p)) {
|
||||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||||
if (_curPeers.isNotEmpty) {
|
if (_curPeers.isNotEmpty) {
|
||||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
@@ -282,14 +308,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
_queryOnlines(bool isLoadEvent) {
|
_queryOnlines(bool isLoadEvent) {
|
||||||
if (_curPeers.isNotEmpty) {
|
if (_curPeers.isNotEmpty) {
|
||||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
|
_queryCount = 0;
|
||||||
|
}
|
||||||
_lastQueryPeers = {..._curPeers};
|
_lastQueryPeers = {..._curPeers};
|
||||||
if (isLoadEvent) {
|
if (isLoadEvent) {
|
||||||
_lastChangeTime = DateTime.now();
|
_lastChangeTime = DateTime.now();
|
||||||
} else {
|
} else {
|
||||||
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
||||||
}
|
}
|
||||||
_queryCount = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Peer>>? matchPeers(
|
Future<List<Peer>>? matchPeers(
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class RawTouchGestureDetectorRegion extends StatefulWidget {
|
|||||||
class _RawTouchGestureDetectorRegionState
|
class _RawTouchGestureDetectorRegionState
|
||||||
extends State<RawTouchGestureDetectorRegion> {
|
extends State<RawTouchGestureDetectorRegion> {
|
||||||
Offset _cacheLongPressPosition = Offset(0, 0);
|
Offset _cacheLongPressPosition = Offset(0, 0);
|
||||||
|
// Timestamp of the last long press event.
|
||||||
|
int _cacheLongPressPositionTs = 0;
|
||||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||||
double _scale = 1;
|
double _scale = 1;
|
||||||
|
|
||||||
@@ -95,20 +97,22 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
// Desktop or mobile "Touch mode"
|
// Desktop or mobile "Touch mode"
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
inputModel.tapDown(MouseButtons.left);
|
inputModel.tapDown(MouseButtons.left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onTapUp(TapUpDetails d) {
|
onTapUp(TapUpDetails d) {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
inputModel.tapUp(MouseButtons.left);
|
inputModel.tapUp(MouseButtons.left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onTap() {
|
onTap() {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
@@ -134,6 +138,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
inputModel.tap(MouseButtons.left);
|
inputModel.tap(MouseButtons.left);
|
||||||
inputModel.tap(MouseButtons.left);
|
inputModel.tap(MouseButtons.left);
|
||||||
}
|
}
|
||||||
@@ -146,6 +153,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
_cacheLongPressPosition = d.localPosition;
|
_cacheLongPressPosition = d.localPosition;
|
||||||
|
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,9 +230,22 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||||
}
|
}
|
||||||
|
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
||||||
|
// If the time interval between the start event and the first pan event is less than 500ms,
|
||||||
|
// we consider to use the long press position as the start position.
|
||||||
|
//
|
||||||
|
// TODO: We should find a better way to send the first pan event as soon as possible.
|
||||||
|
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
|
||||||
|
500) {
|
||||||
|
ffi.cursorModel
|
||||||
|
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||||
|
}
|
||||||
inputModel.sendMouse('down', MouseButtons.left);
|
inputModel.sendMouse('down', MouseButtons.left);
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
} else {
|
} else {
|
||||||
@@ -244,6 +265,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +305,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// mobile
|
// mobile
|
||||||
ffi.canvasModel.updateScale(d.scale / _scale);
|
ffi.canvasModel.updateScale(d.scale / _scale, d.focalPoint);
|
||||||
_scale = d.scale;
|
_scale = d.scale;
|
||||||
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
||||||
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
||||||
|
|||||||
@@ -234,12 +234,12 @@ List<(String, String)> otherDefaultSettings() {
|
|||||||
('True color (4:4:4)', kOptionI444),
|
('True color (4:4:4)', kOptionI444),
|
||||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||||
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||||
if (isDesktop && bind.mainGetUseTextureRender())
|
if (isDesktop)
|
||||||
(
|
(
|
||||||
'Show displays as individual windows',
|
'Show displays as individual windows',
|
||||||
kKeyShowDisplaysAsIndividualWindows
|
kKeyShowDisplaysAsIndividualWindows
|
||||||
),
|
),
|
||||||
if (isDesktop && bind.mainGetUseTextureRender())
|
if (isDesktop)
|
||||||
(
|
(
|
||||||
'Use all my displays for the remote session',
|
'Use all my displays for the remote session',
|
||||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter_hbb/common.dart';
|
|||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -22,6 +23,20 @@ class TTextMenu {
|
|||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.divider = false});
|
this.divider = false});
|
||||||
|
|
||||||
|
Widget getChild() {
|
||||||
|
if (trailingIcon != null) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
trailingIcon!,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TRadioMenu<T> {
|
class TRadioMenu<T> {
|
||||||
@@ -115,12 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// paste
|
// paste
|
||||||
if (isMobile &&
|
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
|
||||||
pi.platform != kPeerPlatformAndroid &&
|
|
||||||
perms['keyboard'] != false &&
|
|
||||||
perms['clipboard'] != false) {
|
|
||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
child: Text(translate('Paste')),
|
child: Text(translate('Send clipboard keystrokes')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
if (data != null && data.text != null) {
|
if (data != null && data.text != null) {
|
||||||
@@ -583,8 +595,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Lock after session end'))));
|
child: Text(translate('Lock after session end'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bind.mainGetUseTextureRender() &&
|
if (pi.isSupportMultiDisplay &&
|
||||||
pi.isSupportMultiDisplay &&
|
|
||||||
PrivacyModeState.find(id).isEmpty &&
|
PrivacyModeState.find(id).isEmpty &&
|
||||||
pi.displaysCount.value > 1 &&
|
pi.displaysCount.value > 1 &&
|
||||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||||
@@ -596,15 +607,13 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionSetDisplaysAsIndividualWindows(
|
bind.sessionSetDisplaysAsIndividualWindows(
|
||||||
sessionId: sessionId, value: value ? 'Y' : '');
|
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
child: Text(translate('Show displays as individual windows'))));
|
child: Text(translate('Show displays as individual windows'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
||||||
if (bind.mainGetUseTextureRender() &&
|
if (pi.isSupportMultiDisplay && isMultiScreens) {
|
||||||
pi.isSupportMultiDisplay &&
|
|
||||||
isMultiScreens) {
|
|
||||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||||
sessionId: ffi.sessionId) ==
|
sessionId: ffi.sessionId) ==
|
||||||
'Y';
|
'Y';
|
||||||
@@ -613,7 +622,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
|
bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
|
||||||
sessionId: sessionId, value: value ? 'Y' : '');
|
sessionId: sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
child: Text(translate('Use all my displays for the remote session'))));
|
child: Text(translate('Use all my displays for the remote session'))));
|
||||||
}
|
}
|
||||||
@@ -639,6 +648,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
v.addAll(toolbarKeyboardToggles(ffi));
|
v.addAll(toolbarKeyboardToggles(ffi));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// view mode (mobile only, desktop is in keyboard menu)
|
||||||
|
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: ffiModel.viewOnly,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
await bind.sessionToggleOption(
|
||||||
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||||
|
ffiModel.setViewOnly(id, value);
|
||||||
|
},
|
||||||
|
child: Text(translate('View Mode'))));
|
||||||
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,3 +800,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
|||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool showVirtualDisplayMenu(FFI ffi) {
|
||||||
|
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ffi.ffiModel.pi.isInstalled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> getVirtualDisplayMenuChildren(
|
||||||
|
FFI ffi, String id, VoidCallback? clickCallBack) {
|
||||||
|
if (!showVirtualDisplayMenu(ffi)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final pi = ffi.ffiModel.pi;
|
||||||
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
|
if (pi.isRustDeskIdd) {
|
||||||
|
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
||||||
|
final children = <Widget>[];
|
||||||
|
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
||||||
|
children.add(Obx(() => CkbMenuButton(
|
||||||
|
value: virtualDisplays.contains(i + 1),
|
||||||
|
onChanged: privacyModeState.isNotEmpty
|
||||||
|
? null
|
||||||
|
: (bool? value) async {
|
||||||
|
if (value != null) {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: i + 1, on: value);
|
||||||
|
clickCallBack?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('${translate('Virtual display')} ${i + 1}'),
|
||||||
|
ffi: ffi,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
children.add(Divider());
|
||||||
|
children.add(Obx(() => MenuButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
index: kAllVirtualDisplay,
|
||||||
|
on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
ffi: ffi,
|
||||||
|
child: Text(translate('Plug out all')),
|
||||||
|
)));
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
if (pi.isAmyuniIdd) {
|
||||||
|
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
||||||
|
final children = <Widget>[
|
||||||
|
Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: 0, on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
child: Icon(Icons.remove),
|
||||||
|
),
|
||||||
|
Text(count.toString()),
|
||||||
|
TextButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 4
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: 0, on: true);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Divider(),
|
||||||
|
Obx(() => MenuButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
index: kAllVirtualDisplay,
|
||||||
|
on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
ffi: ffi,
|
||||||
|
child: Text(translate('Plug out all')),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|||||||
@@ -133,9 +133,29 @@ const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
|||||||
const String kOptionEnableCheckUpdate = "enable-check-update";
|
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||||
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||||
|
const String kOptionStopService = "stop-service";
|
||||||
|
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||||
|
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||||
|
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||||
|
|
||||||
|
// buildin opitons
|
||||||
|
const String kOptionHideServerSetting = "hide-server-settings";
|
||||||
|
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||||
|
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||||
|
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||||
|
const String kOptionRemovePresetPasswordWarning =
|
||||||
|
"remove-preset-password-warning";
|
||||||
|
const kHideUsernameOnCard = "hide-username-on-card";
|
||||||
|
const String kOptionHideHelpCards = "hide-help-cards";
|
||||||
|
|
||||||
const String kOptionToggleViewOnly = "view-only";
|
const String kOptionToggleViewOnly = "view-only";
|
||||||
|
|
||||||
|
const String kOptionDisableFloatingWindow = "disable-floating-window";
|
||||||
|
|
||||||
|
const String kOptionKeepScreenOn = "keep-screen-on";
|
||||||
|
|
||||||
|
const String kOptionShowMobileAction = "showMobileActions";
|
||||||
|
|
||||||
const String kUrlActionClose = "close";
|
const String kUrlActionClose = "close";
|
||||||
|
|
||||||
const String kTabLabelHomePage = "Home";
|
const String kTabLabelHomePage = "Home";
|
||||||
@@ -147,6 +167,8 @@ const int kWindowMainId = 0;
|
|||||||
const String kPointerEventKindTouch = "touch";
|
const String kPointerEventKindTouch = "touch";
|
||||||
const String kPointerEventKindMouse = "mouse";
|
const String kPointerEventKindMouse = "mouse";
|
||||||
|
|
||||||
|
const String kKeyFlutterKey = "flutter_key";
|
||||||
|
|
||||||
const String kKeyShowDisplaysAsIndividualWindows =
|
const String kKeyShowDisplaysAsIndividualWindows =
|
||||||
'displays_as_individual_windows';
|
'displays_as_individual_windows';
|
||||||
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||||
@@ -219,9 +241,9 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
|
|||||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||||
const kFullScreenEdgeSize = 0.0;
|
const kFullScreenEdgeSize = 0.0;
|
||||||
const kMaximizeEdgeSize = 0.0;
|
const kMaximizeEdgeSize = 0.0;
|
||||||
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
|
||||||
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
const kWindowResizeEdgeSize = 5.0;
|
||||||
final kWindowBorderWidth = 1.0;
|
const kWindowBorderWidth = 1.0;
|
||||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||||
const kFrameBorderRadius = 12.0;
|
const kFrameBorderRadius = 12.0;
|
||||||
const kFrameClipRRectBorderRadius = 12.0;
|
const kFrameClipRRectBorderRadius = 12.0;
|
||||||
|
|||||||
@@ -169,16 +169,12 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
|
|||||||
final status =
|
final status =
|
||||||
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
||||||
final statusNum = status['status_num'] as int;
|
final statusNum = status['status_num'] as int;
|
||||||
final preStatus = stateGlobal.svcStatus.value;
|
|
||||||
if (statusNum == 0) {
|
if (statusNum == 0) {
|
||||||
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
||||||
} else if (statusNum == -1) {
|
} else if (statusNum == -1) {
|
||||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||||
} else if (statusNum == 1) {
|
} else if (statusNum == 1) {
|
||||||
stateGlobal.svcStatus.value = SvcStatus.ready;
|
stateGlobal.svcStatus.value = SvcStatus.ready;
|
||||||
if (preStatus != SvcStatus.ready) {
|
|
||||||
gFFI.userModel.refreshCurrentUser();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||||
}
|
}
|
||||||
@@ -212,14 +208,14 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (_idController.text.isEmpty) {
|
if (_idController.text.isEmpty) {
|
||||||
() async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||||
if (lastRemoteId != _idController.id) {
|
if (lastRemoteId != _idController.id) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_idController.id = lastRemoteId;
|
_idController.id = lastRemoteId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}();
|
});
|
||||||
}
|
}
|
||||||
Get.put<IDTextEditingController>(_idController);
|
Get.put<IDTextEditingController>(_idController);
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
@@ -261,8 +257,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
@override
|
@override
|
||||||
void onWindowLeaveFullScreen() {
|
void onWindowLeaveFullScreen() {
|
||||||
// Restore edge border to default edge size.
|
// Restore edge border to default edge size.
|
||||||
stateGlobal.resizeEdgeSize.value =
|
stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
|
||||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
|
? kMaximizeEdgeSize
|
||||||
|
: windowResizeEdgeSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -340,7 +337,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
?.merge(TextStyle(height: 1)),
|
?.merge(TextStyle(height: 1)),
|
||||||
).marginOnly(right: 4),
|
).marginOnly(right: 4),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
waitDuration: Duration(milliseconds: 0),
|
waitDuration: Duration(milliseconds: 300),
|
||||||
message: translate("id_input_tip"),
|
message: translate("id_input_tip"),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.help_outline_outlined,
|
Icons.help_outline_outlined,
|
||||||
|
|||||||
@@ -443,14 +443,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (isMacOS) {
|
} else if (isMacOS) {
|
||||||
if (!(bind.isOutgoingOnly() ||
|
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||||
bind.mainIsCanScreenRecording(prompt: false))) {
|
if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
|
||||||
return buildInstallCard("Permissions", "config_screen", "Configure",
|
return buildInstallCard("Permissions", "config_screen", "Configure",
|
||||||
() async {
|
() async {
|
||||||
bind.mainIsCanScreenRecording(prompt: true);
|
bind.mainIsCanScreenRecording(prompt: true);
|
||||||
watchIsCanScreenRecording = true;
|
watchIsCanScreenRecording = true;
|
||||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||||
} else if (!bind.mainIsProcessTrusted(prompt: false)) {
|
} else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
|
||||||
return buildInstallCard("Permissions", "config_acc", "Configure",
|
return buildInstallCard("Permissions", "config_acc", "Configure",
|
||||||
() async {
|
() async {
|
||||||
bind.mainIsProcessTrusted(prompt: true);
|
bind.mainIsProcessTrusted(prompt: true);
|
||||||
@@ -462,7 +462,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
bind.mainIsCanInputMonitoring(prompt: true);
|
bind.mainIsCanInputMonitoring(prompt: true);
|
||||||
watchIsInputMonitoring = true;
|
watchIsInputMonitoring = true;
|
||||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||||
} else if (!svcStopped.value &&
|
} else if (!isOutgoingOnly &&
|
||||||
|
!svcStopped.value &&
|
||||||
bind.mainIsInstalled() &&
|
bind.mainIsInstalled() &&
|
||||||
!bind.mainIsInstalledDaemon(prompt: false)) {
|
!bind.mainIsInstalledDaemon(prompt: false)) {
|
||||||
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
||||||
@@ -545,6 +546,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
String? link,
|
String? link,
|
||||||
bool? closeButton,
|
bool? closeButton,
|
||||||
String? closeOption}) {
|
String? closeOption}) {
|
||||||
|
if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
|
||||||
|
content != 'install_daemon_tip') {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
void closeCard() async {
|
void closeCard() async {
|
||||||
if (closeOption != null) {
|
if (closeOption != null) {
|
||||||
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
||||||
@@ -671,7 +676,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
systemError = error;
|
systemError = error;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
final v = await bind.mainGetOption(key: "stop-service") == "Y";
|
final v = await mainGetBoolOption(kOptionStopService);
|
||||||
if (v != svcStopped.value) {
|
if (v != svcStopped.value) {
|
||||||
svcStopped.value = v;
|
svcStopped.value = v;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -838,7 +843,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPasswordDialog() async {
|
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||||
final pw = await bind.mainGetPermanentPassword();
|
final pw = await bind.mainGetPermanentPassword();
|
||||||
final p0 = TextEditingController(text: pw);
|
final p0 = TextEditingController(text: pw);
|
||||||
final p1 = TextEditingController(text: pw);
|
final p1 = TextEditingController(text: pw);
|
||||||
@@ -878,6 +883,9 @@ void setPasswordDialog() async {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bind.mainSetPermanentPassword(password: pass);
|
bind.mainSetPermanentPassword(password: pass);
|
||||||
|
if (pass.isNotEmpty) {
|
||||||
|
notEmptyCallback?.call();
|
||||||
|
}
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,13 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
final SettingsTabKey initialTabkey;
|
final SettingsTabKey initialTabkey;
|
||||||
static final List<SettingsTabKey> tabKeys = [
|
static final List<SettingsTabKey> tabKeys = [
|
||||||
SettingsTabKey.general,
|
SettingsTabKey.general,
|
||||||
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
|
if (!bind.isOutgoingOnly() &&
|
||||||
|
!bind.isDisableSettings() &&
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
||||||
SettingsTabKey.safety,
|
SettingsTabKey.safety,
|
||||||
if (!bind.isDisableSettings()) SettingsTabKey.network,
|
if (!bind.isDisableSettings() &&
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
|
||||||
|
SettingsTabKey.network,
|
||||||
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
||||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
||||||
SettingsTabKey.plugin,
|
SettingsTabKey.plugin,
|
||||||
@@ -74,7 +78,8 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
|
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
|
State<DesktopSettingPage> createState() =>
|
||||||
|
_DesktopSettingPageState(initialTabkey);
|
||||||
|
|
||||||
static void switch2page(SettingsTabKey page) {
|
static void switch2page(SettingsTabKey page) {
|
||||||
try {
|
try {
|
||||||
@@ -84,8 +89,10 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
||||||
DesktopTabPage.onAddSetting(initialPage: page);
|
DesktopTabPage.onAddSetting(initialPage: page);
|
||||||
PageController controller = Get.find(tag: _kSettingPageControllerTag);
|
PageController controller =
|
||||||
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
|
Get.find<PageController>(tag: _kSettingPageControllerTag);
|
||||||
|
Rx<SettingsTabKey> selected =
|
||||||
|
Get.find<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
|
||||||
selected.value = page;
|
selected.value = page;
|
||||||
controller.jumpToPage(index);
|
controller.jumpToPage(index);
|
||||||
} else {
|
} else {
|
||||||
@@ -105,10 +112,8 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
@override
|
_DesktopSettingPageState(SettingsTabKey initialTabkey) {
|
||||||
void initState() {
|
var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
|
||||||
super.initState();
|
|
||||||
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
|
|
||||||
if (initialIndex == -1) {
|
if (initialIndex == -1) {
|
||||||
initialIndex = 0;
|
initialIndex = 0;
|
||||||
}
|
}
|
||||||
@@ -171,16 +176,32 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _children() {
|
List<Widget> _children() {
|
||||||
final children = [
|
final children = List<Widget>.empty(growable: true);
|
||||||
_General(),
|
for (final tab in DesktopSettingPage.tabKeys) {
|
||||||
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(),
|
switch (tab) {
|
||||||
if (!bind.isDisableSettings()) _Network(),
|
case SettingsTabKey.general:
|
||||||
if (!bind.isIncomingOnly()) _Display(),
|
children.add(const _General());
|
||||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
break;
|
||||||
_Plugin(),
|
case SettingsTabKey.safety:
|
||||||
if (!bind.isDisableAccount()) _Account(),
|
children.add(const _Safety());
|
||||||
_About(),
|
break;
|
||||||
];
|
case SettingsTabKey.network:
|
||||||
|
children.add(const _Network());
|
||||||
|
break;
|
||||||
|
case SettingsTabKey.display:
|
||||||
|
children.add(const _Display());
|
||||||
|
break;
|
||||||
|
case SettingsTabKey.plugin:
|
||||||
|
children.add(const _Plugin());
|
||||||
|
break;
|
||||||
|
case SettingsTabKey.account:
|
||||||
|
children.add(const _Account());
|
||||||
|
break;
|
||||||
|
case SettingsTabKey.about:
|
||||||
|
children.add(const _About());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,11 +336,11 @@ class _GeneralState extends State<_General> {
|
|||||||
children: [
|
children: [
|
||||||
service(),
|
service(),
|
||||||
theme(),
|
theme(),
|
||||||
|
_Card(title: 'Language', children: [language()]),
|
||||||
hwcodec(),
|
hwcodec(),
|
||||||
audio(context),
|
audio(context),
|
||||||
record(context),
|
record(context),
|
||||||
WaylandCard(),
|
WaylandCard(),
|
||||||
_Card(title: 'Language', children: [language()]),
|
|
||||||
other()
|
other()
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: _kListViewBottomMargin));
|
).marginOnly(bottom: _kListViewBottomMargin));
|
||||||
@@ -413,6 +434,12 @@ class _GeneralState extends State<_General> {
|
|||||||
'Check for software update on startup',
|
'Check for software update on startup',
|
||||||
kOptionEnableCheckUpdate,
|
kOptionEnableCheckUpdate,
|
||||||
isServer: false,
|
isServer: false,
|
||||||
|
),
|
||||||
|
if (isWindows && !bind.isOutgoingOnly())
|
||||||
|
_OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Capture screen using DirectX',
|
||||||
|
kOptionDirectxCapture,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@@ -487,26 +514,28 @@ class _GeneralState extends State<_General> {
|
|||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
return AudioInput(builder: (devices, currentDevice, setDevice) {
|
builder(devices, currentDevice, setDevice) {
|
||||||
return _Card(title: 'Audio Input Device', children: [
|
final child = ComboBox(
|
||||||
...devices.map((device) => _Radio<String>(context,
|
keys: devices,
|
||||||
value: device,
|
values: devices,
|
||||||
groupValue: currentDevice,
|
initialKey: currentDevice,
|
||||||
autoNewLine: false,
|
onChanged: (key) async {
|
||||||
label: device, onChanged: (value) {
|
setDevice(key);
|
||||||
setDevice(value);
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}))
|
},
|
||||||
]);
|
).marginOnly(left: _kContentHMargin);
|
||||||
});
|
return _Card(title: 'Audio Input Device', children: [child]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget record(BuildContext context) {
|
Widget record(BuildContext context) {
|
||||||
final showRootDir = isWindows && bind.mainIsInstalled();
|
final showRootDir = isWindows && bind.mainIsInstalled();
|
||||||
return futureBuilder(future: () async {
|
return futureBuilder(future: () async {
|
||||||
String user_dir = await bind.mainVideoSaveDirectory(root: false);
|
String user_dir = bind.mainVideoSaveDirectory(root: false);
|
||||||
String root_dir =
|
String root_dir =
|
||||||
showRootDir ? await bind.mainVideoSaveDirectory(root: true) : '';
|
showRootDir ? bind.mainVideoSaveDirectory(root: true) : '';
|
||||||
bool user_dir_exists = await Directory(user_dir).exists();
|
bool user_dir_exists = await Directory(user_dir).exists();
|
||||||
bool root_dir_exists =
|
bool root_dir_exists =
|
||||||
showRootDir ? await Directory(root_dir).exists() : false;
|
showRootDir ? await Directory(root_dir).exists() : false;
|
||||||
@@ -673,15 +702,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
// Simple temp wrapper for PR check
|
// Simple temp wrapper for PR check
|
||||||
tmpWrapper() {
|
tmpWrapper() {
|
||||||
RxBool has2fa = bind.mainHasValid2FaSync().obs;
|
RxBool has2fa = bind.mainHasValid2FaSync().obs;
|
||||||
|
RxBool hasBot = bind.mainHasValidBotSync().obs;
|
||||||
update() async {
|
update() async {
|
||||||
has2fa.value = bind.mainHasValid2FaSync();
|
has2fa.value = bind.mainHasValid2FaSync();
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChanged(bool? checked) async {
|
onChanged(bool? checked) async {
|
||||||
|
if (checked == false) {
|
||||||
|
CommonConfirmDialog(
|
||||||
|
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
||||||
|
change2fa(callback: update);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
change2fa(callback: update);
|
change2fa(callback: update);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
final tfa = GestureDetector(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
child: Obx(() => Row(
|
child: Obx(() => Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -702,6 +740,77 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
onChanged(!has2fa.value);
|
onChanged(!has2fa.value);
|
||||||
},
|
},
|
||||||
).marginOnly(left: _kCheckBoxLeftMargin);
|
).marginOnly(left: _kCheckBoxLeftMargin);
|
||||||
|
if (!has2fa.value) {
|
||||||
|
return tfa;
|
||||||
|
}
|
||||||
|
updateBot() async {
|
||||||
|
hasBot.value = bind.mainHasValidBotSync();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangedBot(bool? checked) async {
|
||||||
|
if (checked == false) {
|
||||||
|
CommonConfirmDialog(
|
||||||
|
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
|
||||||
|
changeBot(callback: updateBot);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
changeBot(callback: updateBot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final bot = GestureDetector(
|
||||||
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 300),
|
||||||
|
message: translate("enable-bot-tip"),
|
||||||
|
child: InkWell(
|
||||||
|
child: Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: hasBot.value,
|
||||||
|
onChanged: enabled ? onChangedBot : null)
|
||||||
|
.marginOnly(right: 5),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
translate('Telegram bot'),
|
||||||
|
style: TextStyle(
|
||||||
|
color: disabledTextColor(context, enabled)),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
onChangedBot(!hasBot.value);
|
||||||
|
},
|
||||||
|
).marginOnly(left: _kCheckBoxLeftMargin + 30);
|
||||||
|
|
||||||
|
final trust = Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 300),
|
||||||
|
message: translate("enable-trusted-devices-tip"),
|
||||||
|
child: _OptionCheckBox(context, "Enable trusted devices",
|
||||||
|
kOptionEnableTrustedDevices,
|
||||||
|
enabled: !locked, update: (v) {
|
||||||
|
setState(() {});
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (mainGetBoolOptionSync(kOptionEnableTrustedDevices))
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: locked
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
manageTrustedDeviceDialog();
|
||||||
|
},
|
||||||
|
child: Text(translate('Manage trusted devices')))
|
||||||
|
],
|
||||||
|
).marginOnly(left: 30);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [tfa, bot, trust],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmpWrapper();
|
return tmpWrapper();
|
||||||
@@ -826,12 +935,22 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
label: value,
|
label: value,
|
||||||
onChanged: locked
|
onChanged: locked
|
||||||
? null
|
? null
|
||||||
: ((value) {
|
: ((value) async {
|
||||||
() async {
|
callback() async {
|
||||||
await model.setVerificationMethod(
|
await model.setVerificationMethod(
|
||||||
passwordKeys[passwordValues.indexOf(value)]);
|
passwordKeys[passwordValues.indexOf(value)]);
|
||||||
await model.updatePasswordModel();
|
await model.updatePasswordModel();
|
||||||
}();
|
}
|
||||||
|
|
||||||
|
if (value ==
|
||||||
|
passwordValues[passwordKeys
|
||||||
|
.indexOf(kUsePermanentPassword)] &&
|
||||||
|
(await bind.mainGetPermanentPassword())
|
||||||
|
.isEmpty) {
|
||||||
|
setPasswordDialog(notEmptyCallback: callback);
|
||||||
|
} else {
|
||||||
|
await callback();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -924,6 +1043,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
|
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
|
||||||
'allow-only-conn-window-open',
|
'allow-only-conn-window-open',
|
||||||
reverse: false, enabled: enabled),
|
reverse: false, enabled: enabled),
|
||||||
|
if (bind.mainIsInstalled()) unlockPin()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1024,12 +1144,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
bool enabled = !locked;
|
bool enabled = !locked;
|
||||||
// Simple temp wrapper for PR check
|
// Simple temp wrapper for PR check
|
||||||
tmpWrapper() {
|
tmpWrapper() {
|
||||||
RxBool hasWhitelist = (bind.mainGetOptionSync(key: kOptionWhitelist) !=
|
RxBool hasWhitelist = whitelistNotEmpty().obs;
|
||||||
defaultOptionWhitelist)
|
|
||||||
.obs;
|
|
||||||
update() async {
|
update() async {
|
||||||
hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) !=
|
hasWhitelist.value = whitelistNotEmpty();
|
||||||
defaultOptionWhitelist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChanged(bool? checked) async {
|
onChanged(bool? checked) async {
|
||||||
@@ -1140,7 +1257,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
width: 95,
|
width: 95,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: enabled && !locked && isOptFixed,
|
enabled: enabled && !locked && !isOptFixed,
|
||||||
onChanged: (_) => applyEnabled.value = true,
|
onChanged: (_) => applyEnabled.value = true,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(RegExp(
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
@@ -1174,6 +1291,40 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
}(),
|
}(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget unlockPin() {
|
||||||
|
bool enabled = !locked;
|
||||||
|
RxString unlockPin = bind.mainGetUnlockPin().obs;
|
||||||
|
update() async {
|
||||||
|
unlockPin.value = bind.mainGetUnlockPin();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChanged(bool? checked) async {
|
||||||
|
changeUnlockPinDialog(unlockPin.value, update);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kOptionWhitelist);
|
||||||
|
return GestureDetector(
|
||||||
|
child: Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: unlockPin.isNotEmpty,
|
||||||
|
onChanged: enabled && !isOptFixed ? onChanged : null)
|
||||||
|
.marginOnly(right: 5),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
translate('Unlock with PIN'),
|
||||||
|
style: TextStyle(color: disabledTextColor(context, enabled)),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
onTap: enabled
|
||||||
|
? () {
|
||||||
|
onChanged(!unlockPin.isNotEmpty);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
).marginOnly(left: _kCheckBoxLeftMargin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Network extends StatefulWidget {
|
class _Network extends StatefulWidget {
|
||||||
@@ -1193,6 +1344,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
|||||||
super.build(context);
|
super.build(context);
|
||||||
bool enabled = !locked;
|
bool enabled = !locked;
|
||||||
final scrollController = ScrollController();
|
final scrollController = ScrollController();
|
||||||
|
final hideServer =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||||
|
final hideProxy =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||||
return DesktopScrollWrapper(
|
return DesktopScrollWrapper(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@@ -1206,7 +1361,8 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
|||||||
AbsorbPointer(
|
AbsorbPointer(
|
||||||
absorbing: locked,
|
absorbing: locked,
|
||||||
child: Column(children: [
|
child: Column(children: [
|
||||||
server(enabled),
|
if (!hideServer) server(enabled),
|
||||||
|
if (!hideProxy)
|
||||||
_Card(title: 'Proxy', children: [
|
_Card(title: 'Proxy', children: [
|
||||||
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
||||||
enabled: enabled),
|
enabled: enabled),
|
||||||
@@ -1709,7 +1865,7 @@ class _AboutState extends State<_About> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
physics: DraggableNeverScrollableScrollPhysics(),
|
||||||
child: _Card(title: '${translate('About')} RustDesk', children: [
|
child: _Card(title: translate('About RustDesk'), children: [
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -2064,10 +2220,15 @@ Widget _lock(
|
|||||||
Text(translate(label)).marginOnly(left: 5),
|
Text(translate(label)).marginOnly(left: 5),
|
||||||
]).marginSymmetric(vertical: 2)),
|
]).marginSymmetric(vertical: 2)),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
final unlockPin = bind.mainGetUnlockPin();
|
||||||
|
if (unlockPin.isEmpty) {
|
||||||
bool checked = await callMainCheckSuperUserPermission();
|
bool checked = await callMainCheckSuperUserPermission();
|
||||||
if (checked) {
|
if (checked) {
|
||||||
onUnlock();
|
onUnlock();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
checkUnlockPinDialog(unlockPin, onUnlock);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
).marginSymmetric(horizontal: 2, vertical: 4),
|
).marginSymmetric(horizontal: 2, vertical: 4),
|
||||||
).marginOnly(left: _kCardLeftMargin),
|
).marginOnly(left: _kCardLeftMargin),
|
||||||
@@ -2243,6 +2404,7 @@ void changeSocks5Proxy() async {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
if (!isMobile)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
child: Align(
|
child: Align(
|
||||||
@@ -2272,6 +2434,10 @@ void changeSocks5Proxy() async {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
|
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
|
||||||
|
labelText: isMobile ? translate('Server') : null,
|
||||||
|
helperText:
|
||||||
|
isMobile ? translate("default_proxy_tip") : null,
|
||||||
|
helperMaxLines: isMobile ? 3 : null,
|
||||||
),
|
),
|
||||||
controller: proxyController,
|
controller: proxyController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -2282,6 +2448,7 @@ void changeSocks5Proxy() async {
|
|||||||
).marginOnly(bottom: 8),
|
).marginOnly(bottom: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
if (!isMobile)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -2291,6 +2458,9 @@ void changeSocks5Proxy() async {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: userController,
|
controller: userController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: isMobile ? translate('Username') : null,
|
||||||
|
),
|
||||||
enabled: !isOptFixed,
|
enabled: !isOptFixed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -2298,6 +2468,7 @@ void changeSocks5Proxy() async {
|
|||||||
).marginOnly(bottom: 8),
|
).marginOnly(bottom: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
if (!isMobile)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -2308,6 +2479,7 @@ void changeSocks5Proxy() async {
|
|||||||
child: Obx(() => TextField(
|
child: Obx(() => TextField(
|
||||||
obscureText: obscure.value,
|
obscureText: obscure.value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
labelText: isMobile ? translate('Password') : null,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: () => obscure.value = !obscure.value,
|
onPressed: () => obscure.value = !obscure.value,
|
||||||
icon: Icon(obscure.value
|
icon: Icon(obscure.value
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
|||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
// import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
static void onAddSetting(
|
static void onAddSetting(
|
||||||
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
||||||
try {
|
try {
|
||||||
DesktopTabController tabController = Get.find();
|
DesktopTabController tabController = Get.find<DesktopTabController>();
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: kTabLabelSettingPage,
|
key: kTabLabelSettingPage,
|
||||||
label: kTabLabelSettingPage,
|
label: kTabLabelSettingPage,
|
||||||
@@ -36,14 +37,16 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopTabPageState extends State<DesktopTabPage> {
|
class _DesktopTabPageState extends State<DesktopTabPage>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||||
|
|
||||||
@override
|
final RxBool _block = false.obs;
|
||||||
void initState() {
|
// bool mouseIn = false;
|
||||||
super.initState();
|
|
||||||
Get.put<DesktopTabController>(tabController);
|
_DesktopTabPageState() {
|
||||||
RemoteCountState.init();
|
RemoteCountState.init();
|
||||||
|
Get.put<DesktopTabController>(tabController);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: kTabLabelHomePage,
|
key: kTabLabelHomePage,
|
||||||
label: kTabLabelHomePage,
|
label: kTabLabelHomePage,
|
||||||
@@ -66,10 +69,38 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
|
} else if (state == AppLifecycleState.inactive) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
bool _handleKeyEvent(KeyEvent event) {
|
||||||
|
if (!mouseIn && event is KeyDownEvent) {
|
||||||
|
print('key down: ${event.logicalKey}');
|
||||||
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
|
}
|
||||||
|
return false; // allow it to propagate
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
Get.delete<DesktopTabController>();
|
Get.delete<DesktopTabController>();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -88,12 +119,14 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
isClose: false,
|
isClose: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
blockTab: _block,
|
||||||
)));
|
)));
|
||||||
return isMacOS || kUseCompatibleUiMode
|
return isMacOS || kUseCompatibleUiMode
|
||||||
? tabWidget
|
? tabWidget
|
||||||
: Obx(
|
: Obx(
|
||||||
() => DragToResizeArea(
|
() => DragToResizeArea(
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||||
child: tabWidget,
|
child: tabWidget,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -92,13 +92,16 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
_ffi.dialogManager
|
_ffi.dialogManager
|
||||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||||
});
|
});
|
||||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
|
||||||
if (!isLinux) {
|
if (!isLinux) {
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
}
|
}
|
||||||
debugPrint("File manager page init success with id ${widget.id}");
|
debugPrint("File manager page init success with id ${widget.id}");
|
||||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||||
|
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
widget.tabController.onSelected?.call(widget.id);
|
widget.tabController.onSelected?.call(widget.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -259,6 +262,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Offstage(
|
Offstage(
|
||||||
offstage: item.state != JobState.paused,
|
offstage: item.state != JobState.paused,
|
||||||
child: MenuButton(
|
child: MenuButton(
|
||||||
|
tooltip: translate("Resume"),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
jobController.resumeJob(item.id);
|
jobController.resumeJob(item.id);
|
||||||
},
|
},
|
||||||
@@ -271,6 +275,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate("Delete"),
|
||||||
padding: EdgeInsets.only(right: 15),
|
padding: EdgeInsets.only(right: 15),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
"assets/close.svg",
|
"assets/close.svg",
|
||||||
@@ -518,6 +523,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Back'),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: 3,
|
right: 3,
|
||||||
),
|
),
|
||||||
@@ -537,6 +543,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Parent directory'),
|
||||||
child: RotatedBox(
|
child: RotatedBox(
|
||||||
quarterTurns: 3,
|
quarterTurns: 3,
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
@@ -601,6 +608,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
switch (_locationStatus.value) {
|
switch (_locationStatus.value) {
|
||||||
case LocationStatus.bread:
|
case LocationStatus.bread:
|
||||||
return MenuButton(
|
return MenuButton(
|
||||||
|
tooltip: translate('Search'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_locationStatus.value = LocationStatus.fileSearchBar;
|
_locationStatus.value = LocationStatus.fileSearchBar;
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
@@ -627,6 +635,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
);
|
);
|
||||||
case LocationStatus.fileSearchBar:
|
case LocationStatus.fileSearchBar:
|
||||||
return MenuButton(
|
return MenuButton(
|
||||||
|
tooltip: translate('Clear'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onSearchText("", isLocal);
|
onSearchText("", isLocal);
|
||||||
_locationStatus.value = LocationStatus.bread;
|
_locationStatus.value = LocationStatus.bread;
|
||||||
@@ -642,6 +651,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Refresh File'),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: 3,
|
left: 3,
|
||||||
),
|
),
|
||||||
@@ -667,6 +677,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Home'),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: 3,
|
right: 3,
|
||||||
),
|
),
|
||||||
@@ -682,11 +693,27 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
hoverColor: Theme.of(context).hoverColor,
|
hoverColor: Theme.of(context).hoverColor,
|
||||||
),
|
),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
|
tooltip: translate('Create Folder'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final name = TextEditingController();
|
final name = TextEditingController();
|
||||||
|
String? errorText;
|
||||||
_ffi.dialogManager.show((setState, close, context) {
|
_ffi.dialogManager.show((setState, close, context) {
|
||||||
|
name.addListener(() {
|
||||||
|
if (errorText != null) {
|
||||||
|
setState(() {
|
||||||
|
errorText = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
submit() {
|
submit() {
|
||||||
if (name.value.text.isNotEmpty) {
|
if (name.value.text.isNotEmpty) {
|
||||||
|
if (!PathUtil.validName(name.value.text,
|
||||||
|
controller.options.value.isWindows)) {
|
||||||
|
setState(() {
|
||||||
|
errorText = translate("Invalid folder name");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
controller.createDir(PathUtil.join(
|
controller.createDir(PathUtil.join(
|
||||||
controller.directory.value.path,
|
controller.directory.value.path,
|
||||||
name.value.text,
|
name.value.text,
|
||||||
@@ -718,6 +745,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
labelText: translate(
|
labelText: translate(
|
||||||
"Please enter the folder name",
|
"Please enter the folder name",
|
||||||
),
|
),
|
||||||
|
errorText: errorText,
|
||||||
),
|
),
|
||||||
controller: name,
|
controller: name,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -751,6 +779,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
hoverColor: Theme.of(context).hoverColor,
|
hoverColor: Theme.of(context).hoverColor,
|
||||||
),
|
),
|
||||||
Obx(() => MenuButton(
|
Obx(() => MenuButton(
|
||||||
|
tooltip: translate('Delete'),
|
||||||
onPressed: SelectedItems.valid(selectedItems.items)
|
onPressed: SelectedItems.valid(selectedItems.items)
|
||||||
? () async {
|
? () async {
|
||||||
await (controller
|
await (controller
|
||||||
@@ -882,6 +911,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||||
},
|
},
|
||||||
child: MenuButton(
|
child: MenuButton(
|
||||||
|
tooltip: translate('More'),
|
||||||
onPressed: () => mod_menu.showMenu(
|
onPressed: () => mod_menu.showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
position: menuPos,
|
position: menuPos,
|
||||||
@@ -971,6 +1001,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
final lastModifiedStr = entry.isDrive
|
final lastModifiedStr = entry.isDrive
|
||||||
? " "
|
? " "
|
||||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||||
|
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 1),
|
padding: EdgeInsets.symmetric(vertical: 1),
|
||||||
child: Obx(() => Container(
|
child: Obx(() => Container(
|
||||||
@@ -1035,6 +1066,35 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
_onSelectedChanged(
|
_onSelectedChanged(
|
||||||
items, filteredEntries, entry, isLocal);
|
items, filteredEntries, entry, isLocal);
|
||||||
},
|
},
|
||||||
|
onSecondaryTap: () {
|
||||||
|
final items = [
|
||||||
|
if (!entry.isDrive &&
|
||||||
|
versionCmp(_ffi.ffiModel.pi.version,
|
||||||
|
"1.3.0") >=
|
||||||
|
0)
|
||||||
|
mod_menu.PopupMenuItem(
|
||||||
|
child: Text("Rename"),
|
||||||
|
height: CustomPopupMenuTheme.height,
|
||||||
|
onTap: () {
|
||||||
|
controller.renameAction(entry, isLocal);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
mod_menu.showMenu(
|
||||||
|
context: context,
|
||||||
|
position: secondaryPosition,
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSecondaryTapDown: (details) {
|
||||||
|
secondaryPosition = RelativeRect.fromLTRB(
|
||||||
|
details.globalPosition.dx,
|
||||||
|
details.globalPosition.dy,
|
||||||
|
details.globalPosition.dx,
|
||||||
|
details.globalPosition.dy);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(windowId())
|
||||||
.setTitle(getWindowNameWithId(id));
|
.setTitle(getWindowNameWithId(id));
|
||||||
};
|
};
|
||||||
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: params['id'],
|
key: params['id'],
|
||||||
label: params['id'],
|
label: params['id'],
|
||||||
@@ -54,8 +55,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
|
||||||
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
print(
|
print(
|
||||||
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
||||||
@@ -97,6 +96,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
controller: tabController,
|
controller: tabController,
|
||||||
onWindowCloseButton: handleWindowCloseButton,
|
onWindowCloseButton: handleWindowCloseButton,
|
||||||
tail: const AddButton(),
|
tail: const AddButton(),
|
||||||
|
selectedBorderColor: MyTheme.accent,
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
));
|
));
|
||||||
final tabWidget = isLinux
|
final tabWidget = isLinux
|
||||||
@@ -111,6 +111,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
: SubWindowDragToResizeArea(
|
: SubWindowDragToResizeArea(
|
||||||
child: tabWidget,
|
child: tabWidget,
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||||
windowId: stateGlobal.windowId,
|
windowId: stateGlobal.windowId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
|
|||||||
class _InstallPageState extends State<InstallPage> {
|
class _InstallPageState extends State<InstallPage> {
|
||||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||||
|
|
||||||
@override
|
_InstallPageState() {
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
Get.put<DesktopTabController>(tabController);
|
Get.put<DesktopTabController>(tabController);
|
||||||
const label = "install";
|
const label = "install";
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DragToResizeArea(
|
return DragToResizeArea(
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
|||||||
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_InstallPageBodyState() {
|
||||||
|
controller = TextEditingController(text: bind.installInstallPath());
|
||||||
|
final installOptions = jsonDecode(bind.installInstallOptions());
|
||||||
|
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
|
||||||
|
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
controller = TextEditingController(text: bind.installInstallPath());
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
|||||||
if (desktopicon.value) args += ' desktopicon';
|
if (desktopicon.value) args += ' desktopicon';
|
||||||
bind.installInstallMe(options: args, path: controller.text);
|
bind.installInstallMe(options: args, path: controller.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_install();
|
do_install();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,12 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
isSharedPassword: widget.isSharedPassword,
|
isSharedPassword: widget.isSharedPassword,
|
||||||
forceRelay: widget.forceRelay,
|
forceRelay: widget.forceRelay,
|
||||||
isRdp: widget.isRDP);
|
isRdp: widget.isRDP);
|
||||||
Get.put(_ffi, tag: 'pf_${widget.id}');
|
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||||
debugPrint("Port forward page init success with id ${widget.id}");
|
debugPrint("Port forward page init success with id ${widget.id}");
|
||||||
|
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
widget.tabController.onSelected?.call(widget.id);
|
widget.tabController.onSelected?.call(widget.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -141,8 +144,9 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
|
||||||
|
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context)
|
data: Theme.of(context).copyWith(
|
||||||
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
|
colorScheme: Theme.of(context).colorScheme,
|
||||||
|
),
|
||||||
child: Obx(() => ListView.builder(
|
child: Obx(() => ListView.builder(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemCount: pfs.length + 2,
|
itemCount: pfs.length + 2,
|
||||||
@@ -289,7 +293,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
).marginOnly(left: _kTextLeftMargin));
|
).marginOnly(left: _kTextLeftMargin));
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context)
|
data: Theme.of(context)
|
||||||
.copyWith(backgroundColor: Theme.of(context).colorScheme.background),
|
.copyWith(colorScheme: Theme.of(context).colorScheme),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemCount: 2,
|
itemCount: 2,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(windowId())
|
||||||
.setTitle(getWindowNameWithId(id));
|
.setTitle(getWindowNameWithId(id));
|
||||||
};
|
};
|
||||||
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: params['id'],
|
key: params['id'],
|
||||||
label: params['id'],
|
label: params['id'],
|
||||||
@@ -54,8 +55,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
|
||||||
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||||
@@ -106,6 +105,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
tail: AddButton(),
|
tail: AddButton(),
|
||||||
|
selectedBorderColor: MyTheme.accent,
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -127,6 +127,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
() => SubWindowDragToResizeArea(
|
() => SubWindowDragToResizeArea(
|
||||||
child: tabWidget,
|
child: tabWidget,
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||||
windowId: stateGlobal.windowId,
|
windowId: stateGlobal.windowId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ class RemotePage extends StatefulWidget {
|
|||||||
this.switchUuid,
|
this.switchUuid,
|
||||||
this.forceRelay,
|
this.forceRelay,
|
||||||
this.isSharedPassword,
|
this.isSharedPassword,
|
||||||
}) : super(key: key);
|
}) : super(key: key) {
|
||||||
|
initSharedStates(id);
|
||||||
|
}
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final SessionID? sessionId;
|
final SessionID? sessionId;
|
||||||
@@ -64,7 +66,7 @@ class RemotePage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<RemotePage> createState() {
|
State<RemotePage> createState() {
|
||||||
final state = _RemotePageState();
|
final state = _RemotePageState(id);
|
||||||
_lastState.value = state;
|
_lastState.value = state;
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -85,14 +87,20 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||||
|
|
||||||
|
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
||||||
|
// to identify the toolbar instance and its callback function.
|
||||||
|
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
||||||
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
||||||
|
|
||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
|
|
||||||
SessionID get sessionId => _ffi.sessionId;
|
SessionID get sessionId => _ffi.sessionId;
|
||||||
|
|
||||||
|
_RemotePageState(String id) {
|
||||||
|
_initStates(id);
|
||||||
|
}
|
||||||
|
|
||||||
void _initStates(String id) {
|
void _initStates(String id) {
|
||||||
initSharedStates(id);
|
|
||||||
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||||
@@ -102,9 +110,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initStates(widget.id);
|
|
||||||
_ffi = FFI(widget.sessionId);
|
_ffi = FFI(widget.sessionId);
|
||||||
Get.put(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
@@ -131,11 +138,14 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// Session option should be set after models.dart/FFI.start
|
// Session option should be set after models.dart/FFI.start
|
||||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: kOptionZoomCursor);
|
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||||
|
});
|
||||||
DesktopMultiWindow.addListener(this);
|
DesktopMultiWindow.addListener(this);
|
||||||
// if (!_isCustomCursorInited) {
|
// if (!_isCustomCursorInited) {
|
||||||
// customCursorController.registerNeedUpdateCursorCallback(
|
// customCursorController.registerNeedUpdateCursorCallback(
|
||||||
@@ -150,7 +160,10 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
_blockableOverlayState.applyFfi(_ffi);
|
_blockableOverlayState.applyFfi(_ffi);
|
||||||
|
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
widget.tabController?.onSelected?.call(widget.id);
|
widget.tabController?.onSelected?.call(widget.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -268,9 +281,18 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
id: widget.id,
|
id: widget.id,
|
||||||
ffi: _ffi,
|
ffi: _ffi,
|
||||||
state: widget.toolbarState,
|
state: widget.toolbarState,
|
||||||
onEnterOrLeaveImageSetter: (func) =>
|
onEnterOrLeaveImageSetter: (id, func) {
|
||||||
_onEnterOrLeaveImage4Toolbar = func,
|
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
|
||||||
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
|
_onEnterOrLeaveImage4Toolbar = func;
|
||||||
|
},
|
||||||
|
onEnterOrLeaveImageCleaner: (id) {
|
||||||
|
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
|
||||||
|
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
|
||||||
|
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
|
||||||
|
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
|
||||||
|
_onEnterOrLeaveImage4Toolbar = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
setRemoteState: setState,
|
setRemoteState: setState,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -310,22 +332,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (!_ffi.ffiModel.isPeerAndroid) {
|
if (!_ffi.ffiModel.isPeerAndroid) {
|
||||||
return Offstage();
|
return Offstage();
|
||||||
} else {
|
} else {
|
||||||
if (_ffi.connType == ConnType.defaultConn &&
|
|
||||||
_ffi.ffiModel.permissions['keyboard'] != false) {
|
|
||||||
Timer(
|
|
||||||
Duration(milliseconds: 10),
|
|
||||||
() => _ffi.dialogManager
|
|
||||||
.mobileActionsOverlayVisible.value = true);
|
|
||||||
}
|
|
||||||
return Obx(() => Offstage(
|
return Obx(() => Offstage(
|
||||||
offstage: _ffi.dialogManager
|
offstage: _ffi.dialogManager
|
||||||
.mobileActionsOverlayVisible.isFalse,
|
.mobileActionsOverlayVisible.isFalse,
|
||||||
child: Overlay(initialEntries: [
|
child: Overlay(initialEntries: [
|
||||||
makeMobileActionsOverlayEntry(
|
makeMobileActionsOverlayEntry(
|
||||||
() => _ffi
|
() => _ffi.dialogManager
|
||||||
.dialogManager
|
.setMobileActionsOverlayVisible(false),
|
||||||
.mobileActionsOverlayVisible
|
|
||||||
.value = false,
|
|
||||||
ffi: _ffi,
|
ffi: _ffi,
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
@@ -484,6 +497,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||||
? Container(color: Colors.transparent)
|
? Container(color: Colors.transparent)
|
||||||
: Obx(() {
|
: Obx(() {
|
||||||
|
widget.toolbarState.initShow(sessionId);
|
||||||
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
||||||
return ImagePaint(
|
return ImagePaint(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
@@ -501,12 +515,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!_ffi.canvasModel.cursorEmbedded) {
|
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||||
paints.add(Obx(() => Offstage(
|
paints
|
||||||
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
|
.add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
|
||||||
child: CursorPaint(
|
? Offstage()
|
||||||
|
: CursorPaint(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
zoomCursor: _zoomCursor,
|
zoomCursor: _zoomCursor,
|
||||||
))));
|
)));
|
||||||
}
|
}
|
||||||
paints.add(
|
paints.add(
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -559,11 +574,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
|
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
|
||||||
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final m = Provider.of<ImageModel>(context);
|
final m = Provider.of<ImageModel>(context);
|
||||||
@@ -617,7 +627,8 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
final paintWidth = c.getDisplayWidth() * s;
|
final paintWidth = c.getDisplayWidth() * s;
|
||||||
final paintHeight = c.getDisplayHeight() * s;
|
final paintHeight = c.getDisplayHeight() * s;
|
||||||
final paintSize = Size(paintWidth, paintHeight);
|
final paintSize = Size(paintWidth, paintHeight);
|
||||||
final paintWidget = m.useTextureRender
|
final paintWidget =
|
||||||
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
||||||
? _BuildPaintTextureRender(
|
? _BuildPaintTextureRender(
|
||||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||||
@@ -638,7 +649,8 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
if (c.size.width > 0 && c.size.height > 0) {
|
if (c.size.width > 0 && c.size.height > 0) {
|
||||||
final paintWidget = m.useTextureRender
|
final paintWidget =
|
||||||
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
||||||
? _BuildPaintTextureRender(
|
? _BuildPaintTextureRender(
|
||||||
c,
|
c,
|
||||||
s,
|
s,
|
||||||
@@ -648,7 +660,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
),
|
),
|
||||||
c.size,
|
c.size,
|
||||||
isViewOriginal())
|
isViewOriginal())
|
||||||
: _buildScrollAuthNonTextureRender(m, c, s);
|
: _buildScrollAutoNonTextureRender(m, c, s);
|
||||||
return mouseRegion(child: _buildListener(paintWidget));
|
return mouseRegion(child: _buildListener(paintWidget));
|
||||||
} else {
|
} else {
|
||||||
return Container();
|
return Container();
|
||||||
@@ -664,7 +676,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScrollAuthNonTextureRender(
|
Widget _buildScrollAutoNonTextureRender(
|
||||||
ImageModel m, CanvasModel c, double s) {
|
ImageModel m, CanvasModel c, double s) {
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
size: Size(c.size.width, c.size.height),
|
size: Size(c.size.width, c.size.height),
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||||
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||||
|
|
||||||
late ToolbarState _toolbarState;
|
|
||||||
String? peerId;
|
String? peerId;
|
||||||
bool _isScreenRectSet = false;
|
bool _isScreenRectSet = false;
|
||||||
int? _display;
|
int? _display;
|
||||||
@@ -54,7 +53,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||||
|
|
||||||
_ConnectionTabPageState(Map<String, dynamic> params) {
|
_ConnectionTabPageState(Map<String, dynamic> params) {
|
||||||
_toolbarState = ToolbarState();
|
|
||||||
RemoteCountState.init();
|
RemoteCountState.init();
|
||||||
peerId = params['id'];
|
peerId = params['id'];
|
||||||
final sessionId = params['session_id'];
|
final sessionId = params['session_id'];
|
||||||
@@ -73,7 +71,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final ffi = remotePage.ffi;
|
final ffi = remotePage.ffi;
|
||||||
bind.setCurSessionId(sessionId: ffi.sessionId);
|
bind.setCurSessionId(sessionId: ffi.sessionId);
|
||||||
}
|
}
|
||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(params['windowId'])
|
||||||
.setTitle(getWindowNameWithId(id));
|
.setTitle(getWindowNameWithId(id));
|
||||||
UnreadChatCountState.find(id).value = 0;
|
UnreadChatCountState.find(id).value = 0;
|
||||||
};
|
};
|
||||||
@@ -91,7 +89,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
display: display,
|
display: display,
|
||||||
displays: displays?.cast<int>(),
|
displays: displays?.cast<int>(),
|
||||||
password: params['password'],
|
password: params['password'],
|
||||||
toolbarState: _toolbarState,
|
toolbarState: ToolbarState(),
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
switchUuid: params['switch_uuid'],
|
switchUuid: params['switch_uuid'],
|
||||||
forceRelay: params['forceRelay'],
|
forceRelay: params['forceRelay'],
|
||||||
@@ -100,15 +98,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
));
|
));
|
||||||
_update_remote_count();
|
_update_remote_count();
|
||||||
}
|
}
|
||||||
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||||
|
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
|
||||||
|
|
||||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
|
||||||
if (!_isScreenRectSet) {
|
if (!_isScreenRectSet) {
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
restoreWindowPosition(
|
restoreWindowPosition(
|
||||||
@@ -123,12 +120,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_toolbarState.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final child = Scaffold(
|
final child = Scaffold(
|
||||||
@@ -137,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
controller: tabController,
|
controller: tabController,
|
||||||
onWindowCloseButton: handleWindowCloseButton,
|
onWindowCloseButton: handleWindowCloseButton,
|
||||||
tail: const AddButton(),
|
tail: const AddButton(),
|
||||||
|
selectedBorderColor: MyTheme.accent,
|
||||||
pageViewBuilder: (pageView) => pageView,
|
pageViewBuilder: (pageView) => pageView,
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||||
@@ -236,6 +228,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
// Specially configured for a better resize area and remote control.
|
// Specially configured for a better resize area and remote control.
|
||||||
childPadding: kDragToResizeAreaPadding,
|
childPadding: kDragToResizeAreaPadding,
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
|
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||||
windowId: stateGlobal.windowId,
|
windowId: stateGlobal.windowId,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -251,15 +244,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final pi = ffi.ffiModel.pi;
|
final pi = ffi.ffiModel.pi;
|
||||||
final perms = ffi.ffiModel.permissions;
|
final perms = ffi.ffiModel.permissions;
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
final toolbarState = remotePage.toolbarState;
|
||||||
menu.addAll([
|
menu.addAll([
|
||||||
MenuEntryButton<String>(
|
MenuEntryButton<String>(
|
||||||
childBuilder: (TextStyle? style) => Obx(() => Text(
|
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||||
translate(
|
translate(
|
||||||
_toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||||
style: style,
|
style: style,
|
||||||
)),
|
)),
|
||||||
proc: () {
|
proc: () {
|
||||||
_toolbarState.switchShow();
|
toolbarState.switchShow(sessionId);
|
||||||
cancelFunc();
|
cancelFunc();
|
||||||
},
|
},
|
||||||
padding: padding,
|
padding: padding,
|
||||||
@@ -415,19 +409,19 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
final display = args['display'];
|
final display = args['display'];
|
||||||
final displays = args['displays'];
|
final displays = args['displays'];
|
||||||
final screenRect = parseParamScreenRect(args);
|
final screenRect = parseParamScreenRect(args);
|
||||||
|
final prePeerCount = tabController.length;
|
||||||
Future.delayed(Duration.zero, () async {
|
Future.delayed(Duration.zero, () async {
|
||||||
if (stateGlobal.fullscreen.isTrue) {
|
if (stateGlobal.fullscreen.isTrue) {
|
||||||
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
}
|
}
|
||||||
await setNewConnectWindowFrame(windowId(), id!, screenRect);
|
await setNewConnectWindowFrame(
|
||||||
|
windowId(), id!, prePeerCount, display, screenRect);
|
||||||
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||||
await windowOnTop(windowId());
|
await windowOnTop(windowId());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ConnectionTypeState.init(id);
|
ConnectionTypeState.init(id);
|
||||||
_toolbarState.setShow(
|
|
||||||
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: id,
|
key: id,
|
||||||
label: id,
|
label: id,
|
||||||
@@ -442,7 +436,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
display: display,
|
display: display,
|
||||||
displays: displays?.cast<int>(),
|
displays: displays?.cast<int>(),
|
||||||
password: args['password'],
|
password: args['password'],
|
||||||
toolbarState: _toolbarState,
|
toolbarState: ToolbarState(),
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
switchUuid: switchUuid,
|
switchUuid: switchUuid,
|
||||||
forceRelay: args['forceRelay'],
|
forceRelay: args['forceRelay'],
|
||||||
|
|||||||
@@ -32,14 +32,18 @@ class DesktopServerPage extends StatefulWidget {
|
|||||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||||
with WindowListener, AutomaticKeepAliveClientMixin {
|
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
final tabController = gFFI.serverModel.tabController;
|
final tabController = gFFI.serverModel.tabController;
|
||||||
@override
|
|
||||||
void initState() {
|
_DesktopServerPageState() {
|
||||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
||||||
windowManager.addListener(this);
|
Get.put<DesktopTabController>(tabController);
|
||||||
Get.put(tabController);
|
|
||||||
tabController.onRemoved = (_, id) {
|
tabController.onRemoved = (_, id) {
|
||||||
onRemoveId(id);
|
onRemoveId(id);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
windowManager.addListener(this);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +83,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
child: Consumer<ServerModel>(
|
child: Consumer<ServerModel>(
|
||||||
builder: (context, serverModel, child) {
|
builder: (context, serverModel, child) {
|
||||||
final body = Scaffold(
|
final body = Scaffold(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
body: ConnectionManager(),
|
body: ConnectionManager(),
|
||||||
);
|
);
|
||||||
return isLinux
|
return isLinux
|
||||||
@@ -104,10 +108,11 @@ class ConnectionManager extends StatefulWidget {
|
|||||||
State<StatefulWidget> createState() => ConnectionManagerState();
|
State<StatefulWidget> createState() => ConnectionManagerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConnectionManagerState extends State<ConnectionManager> {
|
class ConnectionManagerState extends State<ConnectionManager>
|
||||||
@override
|
with WidgetsBindingObserver {
|
||||||
void initState() {
|
final RxBool _block = false.obs;
|
||||||
gFFI.serverModel.updateClientState();
|
|
||||||
|
ConnectionManagerState() {
|
||||||
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
||||||
final client_id = int.tryParse(client_id_str);
|
final client_id = int.tryParse(client_id_str);
|
||||||
if (client_id != null) {
|
if (client_id != null) {
|
||||||
@@ -116,7 +121,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
if (client != null) {
|
if (client != null) {
|
||||||
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
||||||
if (client.unreadChatMessageCount.value > 0) {
|
if (client.unreadChatMessageCount.value > 0) {
|
||||||
Future.delayed(Duration.zero, () {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
client.unreadChatMessageCount.value = 0;
|
client.unreadChatMessageCount.value = 0;
|
||||||
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
||||||
});
|
});
|
||||||
@@ -127,9 +132,31 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
gFFI.chatModel.isConnManager = true;
|
gFFI.chatModel.isConnManager = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
if (!allowRemoteCMModification()) {
|
||||||
|
shouldBeBlocked(_block, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
gFFI.serverModel.updateClientState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final serverModel = Provider.of<ServerModel>(context);
|
final serverModel = Provider.of<ServerModel>(context);
|
||||||
@@ -165,8 +192,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
selectedBorderColor: MyTheme.accent,
|
selectedBorderColor: MyTheme.accent,
|
||||||
maxLabelWidth: 100,
|
maxLabelWidth: 100,
|
||||||
tail: null, //buildScrollJumper(),
|
tail: null, //buildScrollJumper(),
|
||||||
selectedTabBackgroundColor:
|
blockTab: allowRemoteCMModification() ? null : _block,
|
||||||
Theme.of(context).hintColor.withOpacity(0),
|
|
||||||
tabBuilder: (key, icon, label, themeConf) {
|
tabBuilder: (key, icon, label, themeConf) {
|
||||||
final client = serverModel.clients
|
final client = serverModel.clients
|
||||||
.firstWhereOrNull((client) => client.id.toString() == key);
|
.firstWhereOrNull((client) => client.id.toString() == key);
|
||||||
@@ -201,27 +227,28 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
borderWidth;
|
borderWidth;
|
||||||
final realChatPageWidth =
|
final realChatPageWidth =
|
||||||
constrains.maxWidth - realClosedWidth;
|
constrains.maxWidth - realClosedWidth;
|
||||||
return Row(children: [
|
final row = Row(children: [
|
||||||
if (constrains.maxWidth >
|
if (constrains.maxWidth >
|
||||||
kConnectionManagerWindowSizeClosedChat.width)
|
kConnectionManagerWindowSizeClosedChat.width)
|
||||||
Consumer<ChatModel>(
|
Consumer<ChatModel>(
|
||||||
builder: (_, model, child) => SizedBox(
|
builder: (_, model, child) => SizedBox(
|
||||||
width: realChatPageWidth,
|
width: realChatPageWidth,
|
||||||
child: buildRemoteBlock(
|
child: allowRemoteCMModification()
|
||||||
child: Container(
|
? buildSidePage()
|
||||||
decoration: BoxDecoration(
|
: buildRemoteBlock(
|
||||||
border: Border(
|
child: buildSidePage(),
|
||||||
right: BorderSide(
|
block: _block,
|
||||||
color: Theme.of(context)
|
mask: true),
|
||||||
.dividerColor))),
|
|
||||||
child: buildSidePage()),
|
|
||||||
),
|
|
||||||
)),
|
)),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: realClosedWidth,
|
width: realClosedWidth,
|
||||||
child:
|
child:
|
||||||
SizedBox(width: realClosedWidth, child: pageView)),
|
SizedBox(width: realClosedWidth, child: pageView)),
|
||||||
]);
|
]);
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: row,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -381,7 +408,10 @@ class _CmHeaderState extends State<_CmHeader>
|
|||||||
_time.value = _time.value + 1;
|
_time.value = _time.value + 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -714,7 +744,8 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
child: buildButton(context,
|
child: buildButton(context,
|
||||||
color: MyTheme.accent,
|
color: MyTheme.accent,
|
||||||
onClick: null, onTapDown: (details) async {
|
onClick: null, onTapDown: (details) async {
|
||||||
final devicesInfo = await AudioInput.getDevicesInfo();
|
final devicesInfo =
|
||||||
|
await AudioInput.getDevicesInfo(true, true);
|
||||||
List<String> devices = devicesInfo['devices'] as List<String>;
|
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
msgBox(
|
msgBox(
|
||||||
@@ -740,13 +771,14 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
value: d,
|
value: d,
|
||||||
height: 18,
|
height: 18,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onTap: () => AudioInput.setDevice(d),
|
onTap: () => AudioInput.setDevice(d, true, true),
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: RadioMenuButton(
|
child: RadioMenuButton(
|
||||||
value: d,
|
value: d,
|
||||||
groupValue: currentDevice,
|
groupValue: currentDevice,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
if (v != null) AudioInput.setDevice(v);
|
if (v != null)
|
||||||
|
AudioInput.setDevice(v, true, true);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1043,6 +1075,10 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void checkClickTime(int id, Function() callback) async {
|
void checkClickTime(int id, Function() callback) async {
|
||||||
|
if (allowRemoteCMModification()) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
await bind.cmCheckClickTime(connId: id);
|
await bind.cmCheckClickTime(connId: id);
|
||||||
Timer(const Duration(milliseconds: 120), () async {
|
Timer(const Duration(milliseconds: 120), () async {
|
||||||
@@ -1051,6 +1087,11 @@ void checkClickTime(int id, Function() callback) async {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool allowRemoteCMModification() {
|
||||||
|
return option2bool(kOptionAllowRemoteCmModification,
|
||||||
|
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
|
||||||
|
}
|
||||||
|
|
||||||
class _FileTransferLogPage extends StatefulWidget {
|
class _FileTransferLogPage extends StatefulWidget {
|
||||||
_FileTransferLogPage({Key? key}) : super(key: key);
|
_FileTransferLogPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -1116,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
|||||||
Text(translate('Create Folder'))
|
Text(translate('Create Folder'))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
case CmFileAction.rename:
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.drive_file_move_outlined,
|
||||||
|
color: Theme.of(context).tabBarTheme.labelColor,
|
||||||
|
),
|
||||||
|
Text(translate('Rename'))
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
|
|||||||
/// The text style of the popup menu item.
|
/// The text style of the popup menu item.
|
||||||
///
|
///
|
||||||
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
|
/// If this property is null, then [PopupMenuThemeData.textStyle] is used.
|
||||||
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1]
|
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium]
|
||||||
/// of [ThemeData.textTheme] is used.
|
/// of [ThemeData.textTheme] is used.
|
||||||
final TextStyle? textStyle;
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
TextStyle style = widget.textStyle ??
|
TextStyle style = widget.textStyle ??
|
||||||
popupMenuTheme.textStyle ??
|
popupMenuTheme.textStyle ??
|
||||||
theme.textTheme.subtitle1!;
|
theme.textTheme.titleMedium!;
|
||||||
|
|
||||||
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
|
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 300),
|
||||||
message: widget.tooltip,
|
message: widget.tooltip,
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
|
|||||||
@@ -38,18 +38,16 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
||||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
|
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(enabled?.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||||
extends State<W> {
|
extends State<W> {
|
||||||
RxBool enabled = true.obs;
|
RxBool enabled = true.obs;
|
||||||
|
|
||||||
@override
|
MyPopupMenuItemState(bool? e) {
|
||||||
void initState() {
|
if (e != null) {
|
||||||
super.initState();
|
enabled.value = e;
|
||||||
if (widget.enabled != null) {
|
|
||||||
enabled.value = widget.enabled!.value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +63,7 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
|||||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
TextStyle style = widget.textStyle ??
|
TextStyle style = widget.textStyle ??
|
||||||
popupMenuTheme.textStyle ??
|
popupMenuTheme.textStyle ??
|
||||||
theme.textTheme.subtitle1!;
|
theme.textTheme.titleMedium!;
|
||||||
return Obx(() => mod_menu.PopupMenuButton<T>(
|
return Obx(() => mod_menu.PopupMenuButton<T>(
|
||||||
enabled: enabled.value,
|
enabled: enabled.value,
|
||||||
position: widget.position,
|
position: widget.position,
|
||||||
|
|||||||
@@ -26,45 +26,42 @@ import './popup_menu.dart';
|
|||||||
import './kb_layout_type_chooser.dart';
|
import './kb_layout_type_chooser.dart';
|
||||||
|
|
||||||
class ToolbarState {
|
class ToolbarState {
|
||||||
late RxBool show;
|
|
||||||
late RxBool _pin;
|
late RxBool _pin;
|
||||||
|
|
||||||
|
bool isShowInited = false;
|
||||||
|
RxBool show = false.obs;
|
||||||
|
|
||||||
ToolbarState() {
|
ToolbarState() {
|
||||||
|
_pin = RxBool(false);
|
||||||
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
||||||
if (s.isEmpty) {
|
if (s.isEmpty) {
|
||||||
_initSet(false, false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final m = jsonDecode(s);
|
final m = jsonDecode(s);
|
||||||
if (m == null) {
|
if (m != null) {
|
||||||
_initSet(false, false);
|
_pin = RxBool(m['pin'] ?? false);
|
||||||
} else {
|
|
||||||
_initSet(m['pin'] ?? false, m['pin'] ?? false);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to decode toolbar state ${e.toString()}');
|
debugPrint('Failed to decode toolbar state ${e.toString()}');
|
||||||
_initSet(false, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initSet(bool s, bool p) {
|
|
||||||
// Show remubar when connection is established.
|
|
||||||
show = RxBool(
|
|
||||||
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
|
||||||
_pin = RxBool(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get pin => _pin.value;
|
bool get pin => _pin.value;
|
||||||
|
|
||||||
switchShow() async {
|
switchShow(SessionID sessionId) async {
|
||||||
|
bind.sessionToggleOption(
|
||||||
|
sessionId: sessionId, value: kOptionCollapseToolbar);
|
||||||
show.value = !show.value;
|
show.value = !show.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShow(bool v) async {
|
initShow(SessionID sessionId) async {
|
||||||
if (show.value != v) {
|
if (!isShowInited) {
|
||||||
show.value = v;
|
show.value = !(await bind.sessionGetToggleOption(
|
||||||
|
sessionId: sessionId, arg: kOptionCollapseToolbar) ??
|
||||||
|
false);
|
||||||
|
isShowInited = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,10 +83,6 @@ class ToolbarState {
|
|||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
||||||
}
|
}
|
||||||
|
|
||||||
save() async {
|
|
||||||
await _savePin();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ToolbarTheme {
|
class _ToolbarTheme {
|
||||||
@@ -332,8 +325,8 @@ class RemoteToolbar extends StatefulWidget {
|
|||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
final ToolbarState state;
|
final ToolbarState state;
|
||||||
final Function(Function(bool)) onEnterOrLeaveImageSetter;
|
final Function(int, Function(bool)) onEnterOrLeaveImageSetter;
|
||||||
final VoidCallback onEnterOrLeaveImageCleaner;
|
final Function(int) onEnterOrLeaveImageCleaner;
|
||||||
final Function(VoidCallback) setRemoteState;
|
final Function(VoidCallback) setRemoteState;
|
||||||
|
|
||||||
RemoteToolbar({
|
RemoteToolbar({
|
||||||
@@ -379,7 +372,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
Future.delayed(Duration.zero, () async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
_fractionX.value = double.tryParse(await bind.sessionGetOption(
|
_fractionX.value = double.tryParse(await bind.sessionGetOption(
|
||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
arg: 'remote-menubar-drag-x') ??
|
arg: 'remote-menubar-drag-x') ??
|
||||||
@@ -393,7 +386,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
initialValue: 0,
|
initialValue: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
widget.onEnterOrLeaveImageSetter((enter) {
|
widget.onEnterOrLeaveImageSetter(identityHashCode(this), (enter) {
|
||||||
if (enter) {
|
if (enter) {
|
||||||
triggerAutoHide();
|
triggerAutoHide();
|
||||||
_isCursorOverImage = true;
|
_isCursorOverImage = true;
|
||||||
@@ -413,12 +406,11 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
dispose() {
|
dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
||||||
widget.onEnterOrLeaveImageCleaner();
|
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// No need to use future builder here.
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Obx(() => show.value
|
child: Obx(() => show.value
|
||||||
@@ -447,7 +439,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
dragging: _dragging,
|
dragging: _dragging,
|
||||||
fractionX: _fractionX,
|
fractionX: _fractionX,
|
||||||
show: show,
|
toolbarState: widget.state,
|
||||||
setFullscreen: _setFullscreen,
|
setFullscreen: _setFullscreen,
|
||||||
setMinimize: _minimize,
|
setMinimize: _minimize,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
@@ -587,8 +579,8 @@ class _MobileActionMenu extends StatelessWidget {
|
|||||||
return Obx(() => _IconMenuButton(
|
return Obx(() => _IconMenuButton(
|
||||||
assetName: 'assets/actions_mobile.svg',
|
assetName: 'assets/actions_mobile.svg',
|
||||||
tooltip: 'Mobile Actions',
|
tooltip: 'Mobile Actions',
|
||||||
onPressed: () =>
|
onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
|
||||||
ffi.dialogManager.mobileActionsOverlayVisible.toggle(),
|
!ffi.dialogManager.mobileActionsOverlayVisible.value),
|
||||||
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
||||||
? _ToolbarTheme.blueColor
|
? _ToolbarTheme.blueColor
|
||||||
: _ToolbarTheme.inactiveColor,
|
: _ToolbarTheme.inactiveColor,
|
||||||
@@ -643,15 +635,12 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
||||||
final m = Provider.of<ImageModel>(context);
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(children: buildMonitorList(context, false)),
|
Row(children: buildMonitorList(context, false)),
|
||||||
supportIndividualWindows && m.useTextureRender ? Divider() : Offstage(),
|
supportIndividualWindows ? Divider() : Offstage(),
|
||||||
supportIndividualWindows && m.useTextureRender
|
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
||||||
? chooseDisplayBehavior()
|
|
||||||
: Offstage(),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -665,7 +654,7 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionSetDisplaysAsIndividualWindows(
|
await bind.sessionSetDisplaysAsIndividualWindows(
|
||||||
sessionId: ffi.sessionId, value: value ? 'Y' : '');
|
sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
|
||||||
},
|
},
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
child: Text(translate('Show displays as individual windows')));
|
child: Text(translate('Show displays as individual windows')));
|
||||||
@@ -737,10 +726,7 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
for (int i = 0; i < pi.displays.length; i++) {
|
for (int i = 0; i < pi.displays.length; i++) {
|
||||||
monitorList.add(buildMonitorButton(i));
|
monitorList.add(buildMonitorButton(i));
|
||||||
}
|
}
|
||||||
final m = Provider.of<ImageModel>(context);
|
if (supportIndividualWindows && pi.displays.length > 1) {
|
||||||
if (supportIndividualWindows &&
|
|
||||||
m.useTextureRender &&
|
|
||||||
pi.displays.length > 1) {
|
|
||||||
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
||||||
}
|
}
|
||||||
return monitorList;
|
return monitorList;
|
||||||
@@ -824,7 +810,6 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
RxInt display = CurrentDisplayState.find(id);
|
RxInt display = CurrentDisplayState.find(id);
|
||||||
if (display.value != i) {
|
if (display.value != i) {
|
||||||
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
||||||
bind.mainGetUseTextureRender() &&
|
|
||||||
bind.sessionGetDisplaysAsIndividualWindows(
|
bind.sessionGetDisplaysAsIndividualWindows(
|
||||||
sessionId: ffi.sessionId) ==
|
sessionId: ffi.sessionId) ==
|
||||||
'Y';
|
'Y';
|
||||||
@@ -1047,11 +1032,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
FFI get ffi => widget.ffi;
|
FFI get ffi => widget.ffi;
|
||||||
String get id => widget.id;
|
String get id => widget.id;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_screenAdjustor.updateScreen();
|
_screenAdjustor.updateScreen();
|
||||||
@@ -1067,15 +1047,11 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
screenAdjustor: _screenAdjustor,
|
screenAdjustor: _screenAdjustor,
|
||||||
),
|
),
|
||||||
if (pi.isRustDeskIdd)
|
if (showVirtualDisplayMenu(ffi))
|
||||||
_RustDeskVirtualDisplayMenu(
|
_SubmenuButton(
|
||||||
id: widget.id,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
if (pi.isAmyuniIdd)
|
|
||||||
_AmyuniVirtualDisplayMenu(
|
|
||||||
id: widget.id,
|
|
||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
|
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
|
||||||
|
child: Text(translate("Virtual display")),
|
||||||
),
|
),
|
||||||
cursorToggles(),
|
cursorToggles(),
|
||||||
Divider(),
|
Divider(),
|
||||||
@@ -1297,7 +1273,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_getLocalResolutionWayland();
|
_getLocalResolutionWayland();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect? scaledRect() {
|
Rect? scaledRect() {
|
||||||
@@ -1574,155 +1552,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
|
|
||||||
final String id;
|
|
||||||
final FFI ffi;
|
|
||||||
|
|
||||||
_RustDeskVirtualDisplayMenu({
|
|
||||||
Key? key,
|
|
||||||
required this.id,
|
|
||||||
required this.ffi,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_RustDeskVirtualDisplayMenu> createState() =>
|
|
||||||
_RustDeskVirtualDisplayMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RustDeskVirtualDisplayMenuState
|
|
||||||
extends State<_RustDeskVirtualDisplayMenu> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
|
||||||
return Offstage();
|
|
||||||
}
|
|
||||||
if (!widget.ffi.ffiModel.pi.isInstalled) {
|
|
||||||
return Offstage();
|
|
||||||
}
|
|
||||||
|
|
||||||
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
|
||||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
|
||||||
|
|
||||||
final children = <Widget>[];
|
|
||||||
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
|
||||||
children.add(Obx(() => CkbMenuButton(
|
|
||||||
value: virtualDisplays.contains(i + 1),
|
|
||||||
onChanged: privacyModeState.isNotEmpty
|
|
||||||
? null
|
|
||||||
: (bool? value) async {
|
|
||||||
if (value != null) {
|
|
||||||
bind.sessionToggleVirtualDisplay(
|
|
||||||
sessionId: widget.ffi.sessionId,
|
|
||||||
index: i + 1,
|
|
||||||
on: value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('${translate('Virtual display')} ${i + 1}'),
|
|
||||||
ffi: widget.ffi,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
children.add(Divider());
|
|
||||||
children.add(Obx(() => MenuButton(
|
|
||||||
onPressed: privacyModeState.isNotEmpty
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
bind.sessionToggleVirtualDisplay(
|
|
||||||
sessionId: widget.ffi.sessionId,
|
|
||||||
index: kAllVirtualDisplay,
|
|
||||||
on: false);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Plug out all')),
|
|
||||||
)));
|
|
||||||
return _SubmenuButton(
|
|
||||||
ffi: widget.ffi,
|
|
||||||
menuChildren: children,
|
|
||||||
child: Text(translate("Virtual display")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _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 {
|
class _KeyboardMenu extends StatelessWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
@@ -1756,6 +1585,7 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
viewMode(),
|
viewMode(),
|
||||||
Divider(),
|
Divider(),
|
||||||
...toolbarToggles(),
|
...toolbarToggles(),
|
||||||
|
...mobileActions(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1892,6 +1722,39 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
child: Text(translate('View Mode')));
|
child: Text(translate('View Mode')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mobileActions() {
|
||||||
|
if (pi.platform != kPeerPlatformAndroid) return [];
|
||||||
|
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
|
||||||
|
if (!enabled) return [];
|
||||||
|
return [
|
||||||
|
Divider(),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Back')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileBack(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Home')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileHome(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Apps')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileApps(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Volume up')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileVolumeUp(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Volume down')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileVolumeDown(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Power')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobilePower(),
|
||||||
|
ffi: ffi),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatMenu extends StatefulWidget {
|
class _ChatMenu extends StatefulWidget {
|
||||||
@@ -1965,8 +1828,8 @@ class _VoiceCallMenu extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
menuChildrenGetter() {
|
menuChildrenGetter() {
|
||||||
final audioInput =
|
final audioInput = AudioInput(
|
||||||
AudioInput(builder: (devices, currentDevice, setDevice) {
|
builder: (devices, currentDevice, setDevice) {
|
||||||
return Column(
|
return Column(
|
||||||
children: devices
|
children: devices
|
||||||
.map((d) => RdoMenuButton<String>(
|
.map((d) => RdoMenuButton<String>(
|
||||||
@@ -1986,7 +1849,10 @@ class _VoiceCallMenu extends StatelessWidget {
|
|||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
isCm: false,
|
||||||
|
isVoiceCall: true,
|
||||||
|
);
|
||||||
return [
|
return [
|
||||||
audioInput,
|
audioInput,
|
||||||
Divider(),
|
Divider(),
|
||||||
@@ -2351,7 +2217,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
final SessionID sessionId;
|
final SessionID sessionId;
|
||||||
final RxDouble fractionX;
|
final RxDouble fractionX;
|
||||||
final RxBool dragging;
|
final RxBool dragging;
|
||||||
final RxBool show;
|
final ToolbarState toolbarState;
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
|
|
||||||
final Function(bool) setFullscreen;
|
final Function(bool) setFullscreen;
|
||||||
@@ -2362,7 +2228,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.fractionX,
|
required this.fractionX,
|
||||||
required this.dragging,
|
required this.dragging,
|
||||||
required this.show,
|
required this.toolbarState,
|
||||||
required this.setFullscreen,
|
required this.setFullscreen,
|
||||||
required this.setMinimize,
|
required this.setMinimize,
|
||||||
required this.borderRadius,
|
required this.borderRadius,
|
||||||
@@ -2378,6 +2244,8 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
double left = 0.0;
|
double left = 0.0;
|
||||||
double right = 1.0;
|
double right = 1.0;
|
||||||
|
|
||||||
|
RxBool get show => widget.toolbarState.show;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -2465,7 +2333,8 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
if (!isMacOS) Obx(() => Offstage(
|
if (!isMacOS)
|
||||||
|
Obx(() => Offstage(
|
||||||
offstage: isFullscreen.isFalse,
|
offstage: isFullscreen.isFalse,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () => widget.setMinimize(),
|
onPressed: () => widget.setMinimize(),
|
||||||
@@ -2480,13 +2349,13 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
)),
|
)),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
widget.show.value = !widget.show.value;
|
widget.toolbarState.switchShow(widget.sessionId);
|
||||||
}),
|
}),
|
||||||
child: Obx((() => Tooltip(
|
child: Obx((() => Tooltip(
|
||||||
message: translate(
|
message:
|
||||||
widget.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
|
show.isTrue ? Icons.expand_less : Icons.expand_more,
|
||||||
size: iconSize,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
))),
|
))),
|
||||||
|
|||||||
@@ -227,11 +227,9 @@ typedef TabMenuBuilder = Widget Function(String key);
|
|||||||
typedef LabelGetter = Rx<String> Function(String key);
|
typedef LabelGetter = Rx<String> Function(String key);
|
||||||
|
|
||||||
/// [_lastClickTime], help to handle double click
|
/// [_lastClickTime], help to handle double click
|
||||||
int _lastClickTime =
|
int _lastClickTime = 0;
|
||||||
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
class DesktopTab extends StatefulWidget {
|
||||||
class DesktopTab extends StatelessWidget {
|
|
||||||
final bool showLogo;
|
final bool showLogo;
|
||||||
final bool showTitle;
|
final bool showTitle;
|
||||||
final bool showMinimize;
|
final bool showMinimize;
|
||||||
@@ -248,15 +246,12 @@ class DesktopTab extends StatelessWidget {
|
|||||||
final Color? selectedTabBackgroundColor;
|
final Color? selectedTabBackgroundColor;
|
||||||
final Color? unSelectedTabBackgroundColor;
|
final Color? unSelectedTabBackgroundColor;
|
||||||
final Color? selectedBorderColor;
|
final Color? selectedBorderColor;
|
||||||
|
final RxBool? blockTab;
|
||||||
|
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
|
|
||||||
Rx<DesktopTabState> get state => controller.state;
|
|
||||||
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
|
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
|
||||||
|
|
||||||
late final DesktopTabType tabType;
|
|
||||||
late final bool isMainWindow;
|
|
||||||
|
|
||||||
final RxList<String> invisibleTabKeys = RxList.empty();
|
final RxList<String> invisibleTabKeys = RxList.empty();
|
||||||
|
|
||||||
DesktopTab({
|
DesktopTab({
|
||||||
@@ -277,38 +272,260 @@ class DesktopTab extends StatelessWidget {
|
|||||||
this.selectedTabBackgroundColor,
|
this.selectedTabBackgroundColor,
|
||||||
this.unSelectedTabBackgroundColor,
|
this.unSelectedTabBackgroundColor,
|
||||||
this.selectedBorderColor,
|
this.selectedBorderColor,
|
||||||
}) : super(key: key) {
|
this.blockTab,
|
||||||
tabType = controller.tabType;
|
}) : super(key: key);
|
||||||
isMainWindow = tabType == DesktopTabType.main ||
|
|
||||||
tabType == DesktopTabType.cm ||
|
|
||||||
tabType == DesktopTabType.install;
|
|
||||||
}
|
|
||||||
|
|
||||||
static RxString tablabelGetter(String peerId) {
|
static RxString tablabelGetter(String peerId) {
|
||||||
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||||
return RxString(getDesktopTabLabel(peerId, alias));
|
return RxString(getDesktopTabLabel(peerId, alias));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DesktopTab> createState() {
|
||||||
|
return _DesktopTabState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class _DesktopTabState extends State<DesktopTab>
|
||||||
|
with MultiWindowListener, WindowListener {
|
||||||
|
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
|
||||||
|
Timer? _macOSCheckRestoreTimer;
|
||||||
|
int _macOSCheckRestoreCounter = 0;
|
||||||
|
|
||||||
|
bool get showLogo => widget.showLogo;
|
||||||
|
bool get showTitle => widget.showTitle;
|
||||||
|
bool get showMinimize => widget.showMinimize;
|
||||||
|
bool get showMaximize => widget.showMaximize;
|
||||||
|
bool get showClose => widget.showClose;
|
||||||
|
Widget Function(Widget pageView)? get pageViewBuilder =>
|
||||||
|
widget.pageViewBuilder;
|
||||||
|
TabMenuBuilder? get tabMenuBuilder => widget.tabMenuBuilder;
|
||||||
|
Widget? get tail => widget.tail;
|
||||||
|
Future<bool> Function()? get onWindowCloseButton =>
|
||||||
|
widget.onWindowCloseButton;
|
||||||
|
TabBuilder? get tabBuilder => widget.tabBuilder;
|
||||||
|
LabelGetter? get labelGetter => widget.labelGetter;
|
||||||
|
double? get maxLabelWidth => widget.maxLabelWidth;
|
||||||
|
Color? get selectedTabBackgroundColor => widget.selectedTabBackgroundColor;
|
||||||
|
Color? get unSelectedTabBackgroundColor =>
|
||||||
|
widget.unSelectedTabBackgroundColor;
|
||||||
|
Color? get selectedBorderColor => widget.selectedBorderColor;
|
||||||
|
RxBool? get blockTab => widget.blockTab;
|
||||||
|
DesktopTabController get controller => widget.controller;
|
||||||
|
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
|
||||||
|
Debouncer get _scrollDebounce => widget._scrollDebounce;
|
||||||
|
|
||||||
|
Rx<DesktopTabState> get state => controller.state;
|
||||||
|
|
||||||
|
DesktopTabType get tabType => controller.tabType;
|
||||||
|
bool get isMainWindow =>
|
||||||
|
tabType == DesktopTabType.main ||
|
||||||
|
tabType == DesktopTabType.cm ||
|
||||||
|
tabType == DesktopTabType.install;
|
||||||
|
|
||||||
|
_DesktopTabState() : super();
|
||||||
|
|
||||||
|
static RxString tablabelGetter(String peerId) {
|
||||||
|
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||||
|
return RxString(getDesktopTabLabel(peerId, alias));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
DesktopMultiWindow.addListener(this);
|
||||||
|
windowManager.addListener(this);
|
||||||
|
|
||||||
|
Future.delayed(Duration(milliseconds: 500), () {
|
||||||
|
if (isMainWindow) {
|
||||||
|
windowManager.isMaximized().then((maximized) {
|
||||||
|
if (stateGlobal.isMaximized.value != maximized) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => setState(() => stateGlobal.setMaximized(maximized)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
final wc = WindowController.fromWindowId(kWindowId!);
|
||||||
|
wc.isMaximized().then((maximized) {
|
||||||
|
debugPrint("isMaximized $maximized");
|
||||||
|
if (stateGlobal.isMaximized.value != maximized) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => setState(() => stateGlobal.setMaximized(maximized)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
DesktopMultiWindow.removeListener(this);
|
||||||
|
windowManager.removeListener(this);
|
||||||
|
_macOSCheckRestoreTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setMaximized(bool maximize) {
|
||||||
|
stateGlobal.setMaximized(maximize);
|
||||||
|
_saveFrameDebounce.call(_saveFrame);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowFocus() {
|
||||||
|
stateGlobal.isFocused.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowBlur() {
|
||||||
|
stateGlobal.isFocused.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMinimize() {
|
||||||
|
stateGlobal.setMinimized(true);
|
||||||
|
stateGlobal.setMaximized(false);
|
||||||
|
super.onWindowMinimize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMaximize() {
|
||||||
|
stateGlobal.setMinimized(false);
|
||||||
|
_setMaximized(true);
|
||||||
|
super.onWindowMaximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowUnmaximize() {
|
||||||
|
stateGlobal.setMinimized(false);
|
||||||
|
_setMaximized(false);
|
||||||
|
super.onWindowUnmaximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveFrame() async {
|
||||||
|
if (tabType == DesktopTabType.main) {
|
||||||
|
await saveWindowPosition(WindowType.Main);
|
||||||
|
} else if (kWindowType != null && kWindowId != null) {
|
||||||
|
await saveWindowPosition(kWindowType!, windowId: kWindowId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMoved() {
|
||||||
|
_saveFrameDebounce.call(_saveFrame);
|
||||||
|
super.onWindowMoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowResized() {
|
||||||
|
_saveFrameDebounce.call(_saveFrame);
|
||||||
|
super.onWindowMoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowClose() async {
|
||||||
|
mainWindowClose() async => await windowManager.hide();
|
||||||
|
notMainWindowClose(WindowController windowController) async {
|
||||||
|
if (controller.length != 0) {
|
||||||
|
debugPrint("close not empty multiwindow from taskbar");
|
||||||
|
if (isWindows) {
|
||||||
|
await windowController.show();
|
||||||
|
await windowController.focus();
|
||||||
|
final res = await onWindowCloseButton?.call() ?? true;
|
||||||
|
if (!res) return;
|
||||||
|
}
|
||||||
|
controller.clear();
|
||||||
|
}
|
||||||
|
await windowController.hide();
|
||||||
|
await rustDeskWinManager
|
||||||
|
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
||||||
|
}
|
||||||
|
|
||||||
|
macOSWindowClose(
|
||||||
|
Future<bool> Function() checkFullscreen,
|
||||||
|
Future<void> Function() closeFunc,
|
||||||
|
) async {
|
||||||
|
_macOSCheckRestoreCounter = 0;
|
||||||
|
_macOSCheckRestoreTimer =
|
||||||
|
Timer.periodic(Duration(milliseconds: 30), (timer) async {
|
||||||
|
_macOSCheckRestoreCounter++;
|
||||||
|
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
|
||||||
|
_macOSCheckRestoreTimer?.cancel();
|
||||||
|
_macOSCheckRestoreTimer = null;
|
||||||
|
Timer(Duration(milliseconds: 700), () async => await closeFunc());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide window on close
|
||||||
|
if (isMainWindow) {
|
||||||
|
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
|
||||||
|
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
|
||||||
|
}
|
||||||
|
// macOS specific workaround, the window is not hiding when in fullscreen.
|
||||||
|
if (isMacOS && await windowManager.isFullScreen()) {
|
||||||
|
await windowManager.setFullScreen(false);
|
||||||
|
await macOSWindowClose(
|
||||||
|
() async => await windowManager.isFullScreen(),
|
||||||
|
mainWindowClose,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await mainWindowClose();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// it's safe to hide the subwindow
|
||||||
|
final controller = WindowController.fromWindowId(kWindowId!);
|
||||||
|
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.
|
||||||
|
|
||||||
|
if (await onWindowCloseButton?.call() ?? true) {
|
||||||
|
if (await controller.isFullScreen()) {
|
||||||
|
await controller.setFullscreen(false);
|
||||||
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
|
await macOSWindowClose(
|
||||||
|
() async => await controller.isFullScreen(),
|
||||||
|
() async => await notMainWindowClose(controller),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await notMainWindowClose(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await notMainWindowClose(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onWindowClose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(children: [
|
return Column(children: [
|
||||||
Obx(() => Offstage(
|
Obx(() {
|
||||||
offstage: !stateGlobal.showTabBar.isTrue ||
|
if (stateGlobal.showTabBar.isTrue &&
|
||||||
(kUseCompatibleUiMode && isHideSingleItem()),
|
!(kUseCompatibleUiMode && isHideSingleItem())) {
|
||||||
child: SizedBox(
|
final showBottomDivider = _showTabBarBottomDivider(tabType);
|
||||||
|
return SizedBox(
|
||||||
height: _kTabBarHeight,
|
height: _kTabBarHeight,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: _kTabBarHeight - 1,
|
height:
|
||||||
|
showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight,
|
||||||
child: _buildBar(),
|
child: _buildBar(),
|
||||||
),
|
),
|
||||||
|
if (showBottomDivider)
|
||||||
const Divider(
|
const Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
))),
|
);
|
||||||
|
} else {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
}),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: pageViewBuilder != null
|
child: pageViewBuilder != null
|
||||||
? pageViewBuilder!(_buildPageView())
|
? pageViewBuilder!(_buildPageView())
|
||||||
@@ -317,19 +534,15 @@ class DesktopTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBlock({required Widget child}) {
|
Widget _buildBlock({required Widget child}) {
|
||||||
if (tabType != DesktopTabType.main) {
|
if (blockTab != null) {
|
||||||
return child;
|
|
||||||
}
|
|
||||||
return buildRemoteBlock(
|
return buildRemoteBlock(
|
||||||
child: child,
|
child: child,
|
||||||
use: () async {
|
block: blockTab!,
|
||||||
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
use: canBeBlocked,
|
||||||
var option = option2bool(
|
mask: tabType == DesktopTabType.main);
|
||||||
kOptionAllowRemoteConfigModification,
|
} else {
|
||||||
await bind.mainGetOption(
|
return child;
|
||||||
key: kOptionAllowRemoteConfigModification));
|
}
|
||||||
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _tabWidgets = [];
|
List<Widget> _tabWidgets = [];
|
||||||
@@ -397,6 +610,8 @@ class DesktopTab extends StatelessWidget {
|
|||||||
: null,
|
: null,
|
||||||
onPanStart: (_) => startDragging(isMainWindow),
|
onPanStart: (_) => startDragging(isMainWindow),
|
||||||
onPanCancel: () {
|
onPanCancel: () {
|
||||||
|
// We want to disable dragging of the tab area in the tab bar.
|
||||||
|
// Disable dragging is needed because macOS handles dragging by default.
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
setMovable(isMainWindow, false);
|
setMovable(isMainWindow, false);
|
||||||
}
|
}
|
||||||
@@ -464,7 +679,6 @@ class DesktopTab extends StatelessWidget {
|
|||||||
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
|
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
|
||||||
WindowActionPanel(
|
WindowActionPanel(
|
||||||
isMainWindow: isMainWindow,
|
isMainWindow: isMainWindow,
|
||||||
tabType: tabType,
|
|
||||||
state: state,
|
state: state,
|
||||||
tabController: controller,
|
tabController: controller,
|
||||||
invisibleTabKeys: invisibleTabKeys,
|
invisibleTabKeys: invisibleTabKeys,
|
||||||
@@ -482,7 +696,6 @@ class DesktopTab extends StatelessWidget {
|
|||||||
|
|
||||||
class WindowActionPanel extends StatefulWidget {
|
class WindowActionPanel extends StatefulWidget {
|
||||||
final bool isMainWindow;
|
final bool isMainWindow;
|
||||||
final DesktopTabType tabType;
|
|
||||||
final Rx<DesktopTabState> state;
|
final Rx<DesktopTabState> state;
|
||||||
final DesktopTabController tabController;
|
final DesktopTabController tabController;
|
||||||
|
|
||||||
@@ -498,7 +711,6 @@ class WindowActionPanel extends StatefulWidget {
|
|||||||
const WindowActionPanel(
|
const WindowActionPanel(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required this.isMainWindow,
|
required this.isMainWindow,
|
||||||
required this.tabType,
|
|
||||||
required this.state,
|
required this.state,
|
||||||
required this.tabController,
|
required this.tabController,
|
||||||
required this.invisibleTabKeys,
|
required this.invisibleTabKeys,
|
||||||
@@ -516,180 +728,7 @@ class WindowActionPanel extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WindowActionPanelState extends State<WindowActionPanel>
|
class WindowActionPanelState extends State<WindowActionPanel> {
|
||||||
with MultiWindowListener, WindowListener {
|
|
||||||
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
|
|
||||||
Timer? _macOSCheckRestoreTimer;
|
|
||||||
int _macOSCheckRestoreCounter = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
DesktopMultiWindow.addListener(this);
|
|
||||||
windowManager.addListener(this);
|
|
||||||
|
|
||||||
Future.delayed(Duration(milliseconds: 500), () {
|
|
||||||
if (widget.isMainWindow) {
|
|
||||||
windowManager.isMaximized().then((maximized) {
|
|
||||||
if (stateGlobal.isMaximized.value != maximized) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) => setState(() => stateGlobal.setMaximized(maximized)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
final wc = WindowController.fromWindowId(kWindowId!);
|
|
||||||
wc.isMaximized().then((maximized) {
|
|
||||||
debugPrint("isMaximized $maximized");
|
|
||||||
if (stateGlobal.isMaximized.value != maximized) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) => setState(() => stateGlobal.setMaximized(maximized)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
DesktopMultiWindow.removeListener(this);
|
|
||||||
windowManager.removeListener(this);
|
|
||||||
_macOSCheckRestoreTimer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setMaximized(bool maximize) {
|
|
||||||
stateGlobal.setMaximized(maximize);
|
|
||||||
_saveFrameDebounce.call(_saveFrame);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowFocus() {
|
|
||||||
stateGlobal.isFocused.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowBlur() {
|
|
||||||
stateGlobal.isFocused.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowMinimize() {
|
|
||||||
stateGlobal.setMinimized(true);
|
|
||||||
stateGlobal.setMaximized(false);
|
|
||||||
super.onWindowMinimize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowMaximize() {
|
|
||||||
stateGlobal.setMinimized(false);
|
|
||||||
_setMaximized(true);
|
|
||||||
super.onWindowMaximize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowUnmaximize() {
|
|
||||||
stateGlobal.setMinimized(false);
|
|
||||||
_setMaximized(false);
|
|
||||||
super.onWindowUnmaximize();
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveFrame() async {
|
|
||||||
if (widget.tabType == DesktopTabType.main) {
|
|
||||||
await saveWindowPosition(WindowType.Main);
|
|
||||||
} else if (kWindowType != null && kWindowId != null) {
|
|
||||||
await saveWindowPosition(kWindowType!, windowId: kWindowId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowMoved() {
|
|
||||||
_saveFrameDebounce.call(_saveFrame);
|
|
||||||
super.onWindowMoved();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowResized() {
|
|
||||||
_saveFrameDebounce.call(_saveFrame);
|
|
||||||
super.onWindowMoved();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowClose() async {
|
|
||||||
mainWindowClose() async => await windowManager.hide();
|
|
||||||
notMainWindowClose(WindowController controller) async {
|
|
||||||
if (widget.tabController.length != 0) {
|
|
||||||
debugPrint("close not empty multiwindow from taskbar");
|
|
||||||
if (isWindows) {
|
|
||||||
await controller.show();
|
|
||||||
await controller.focus();
|
|
||||||
final res = await widget.onClose?.call() ?? true;
|
|
||||||
if (!res) return;
|
|
||||||
}
|
|
||||||
widget.tabController.clear();
|
|
||||||
}
|
|
||||||
await controller.hide();
|
|
||||||
await rustDeskWinManager
|
|
||||||
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
|
||||||
}
|
|
||||||
|
|
||||||
macOSWindowClose(
|
|
||||||
Future<bool> Function() checkFullscreen,
|
|
||||||
Future<void> Function() closeFunc,
|
|
||||||
) async {
|
|
||||||
_macOSCheckRestoreCounter = 0;
|
|
||||||
_macOSCheckRestoreTimer =
|
|
||||||
Timer.periodic(Duration(milliseconds: 30), (timer) async {
|
|
||||||
_macOSCheckRestoreCounter++;
|
|
||||||
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
|
|
||||||
_macOSCheckRestoreTimer?.cancel();
|
|
||||||
_macOSCheckRestoreTimer = null;
|
|
||||||
Timer(Duration(milliseconds: 700), () async => await closeFunc());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide window on close
|
|
||||||
if (widget.isMainWindow) {
|
|
||||||
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
|
|
||||||
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
|
|
||||||
}
|
|
||||||
// macOS specific workaround, the window is not hiding when in fullscreen.
|
|
||||||
if (isMacOS && await windowManager.isFullScreen()) {
|
|
||||||
await windowManager.setFullScreen(false);
|
|
||||||
await macOSWindowClose(
|
|
||||||
() async => await windowManager.isFullScreen(),
|
|
||||||
mainWindowClose,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await mainWindowClose();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// it's safe to hide the subwindow
|
|
||||||
final controller = WindowController.fromWindowId(kWindowId!);
|
|
||||||
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.
|
|
||||||
|
|
||||||
if (await widget.onClose?.call() ?? true) {
|
|
||||||
if (await controller.isFullScreen()) {
|
|
||||||
await controller.setFullscreen(false);
|
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
|
||||||
await macOSWindowClose(
|
|
||||||
() async => await controller.isFullScreen(),
|
|
||||||
() async => await notMainWindowClose(controller),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await notMainWindowClose(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await notMainWindowClose(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onWindowClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool showTabDowndown() {
|
bool showTabDowndown() {
|
||||||
return widget.tabController.state.value.tabs.length > 1 &&
|
return widget.tabController.state.value.tabs.length > 1 &&
|
||||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||||
@@ -710,22 +749,22 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Obx(() => Offstage(
|
Obx(() {
|
||||||
offstage:
|
if (showTabDowndown() && existingInvisibleTab().isNotEmpty) {
|
||||||
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
|
return _TabDropDownButton(
|
||||||
child: _TabDropDownButton(
|
|
||||||
controller: widget.tabController,
|
controller: widget.tabController,
|
||||||
labelGetter: widget.labelGetter,
|
labelGetter: widget.labelGetter,
|
||||||
tabkeys: existingInvisibleTab()),
|
tabkeys: existingInvisibleTab());
|
||||||
)),
|
} else {
|
||||||
Offstage(offstage: widget.tail == null, child: widget.tail),
|
return Offstage();
|
||||||
Offstage(
|
}
|
||||||
offstage: kUseCompatibleUiMode,
|
}),
|
||||||
child: Row(
|
if (widget.tail != null) widget.tail!,
|
||||||
|
if (!kUseCompatibleUiMode)
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
if (widget.showMinimize && !isMacOS)
|
||||||
offstage: !widget.showMinimize || isMacOS,
|
ActionIcon(
|
||||||
child: ActionIcon(
|
|
||||||
message: 'Minimize',
|
message: 'Minimize',
|
||||||
icon: IconFont.min,
|
icon: IconFont.min,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -736,10 +775,9 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
isClose: false,
|
isClose: false,
|
||||||
)),
|
),
|
||||||
Offstage(
|
if (widget.showMaximize && !isMacOS)
|
||||||
offstage: !widget.showMaximize || isMacOS,
|
Obx(() => ActionIcon(
|
||||||
child: Obx(() => ActionIcon(
|
|
||||||
message: stateGlobal.isMaximized.isTrue
|
message: stateGlobal.isMaximized.isTrue
|
||||||
? 'Restore'
|
? 'Restore'
|
||||||
: 'Maximize',
|
: 'Maximize',
|
||||||
@@ -750,10 +788,9 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
? null
|
? null
|
||||||
: _toggleMaximize,
|
: _toggleMaximize,
|
||||||
isClose: false,
|
isClose: false,
|
||||||
))),
|
)),
|
||||||
Offstage(
|
if (widget.showClose && !isMacOS)
|
||||||
offstage: !widget.showClose || isMacOS,
|
ActionIcon(
|
||||||
child: ActionIcon(
|
|
||||||
message: 'Close',
|
message: 'Close',
|
||||||
icon: IconFont.close,
|
icon: IconFont.close,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -772,10 +809,9 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
isClose: true,
|
isClose: true,
|
||||||
))
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1128,7 +1164,10 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: _kTabBarHeight,
|
// _kTabBarHeight also displays normally
|
||||||
|
height: _showTabBarBottomDivider(widget.tabType)
|
||||||
|
? _kTabBarHeight - 1
|
||||||
|
: _kTabBarHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -1180,9 +1219,9 @@ class _CloseButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _kIconSize,
|
width: _kIconSize,
|
||||||
child: Offstage(
|
child: () {
|
||||||
offstage: !visible,
|
if (visible) {
|
||||||
child: InkWell(
|
return InkWell(
|
||||||
hoverColor: MyTheme.tabbar(context).closeHoverColor,
|
hoverColor: MyTheme.tabbar(context).closeHoverColor,
|
||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
onTap: () => onClose(),
|
onTap: () => onClose(),
|
||||||
@@ -1193,8 +1232,12 @@ class _CloseButton extends StatelessWidget {
|
|||||||
? MyTheme.tabbar(context).selectedIconColor
|
? MyTheme.tabbar(context).selectedIconColor
|
||||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
)).paddingOnly(left: 10);
|
} else {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
}())
|
||||||
|
.paddingOnly(left: 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1223,13 +1266,7 @@ class ActionIcon extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ActionIconState extends State<ActionIcon> {
|
class _ActionIconState extends State<ActionIcon> {
|
||||||
var hover = false.obs;
|
final hover = false.obs;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
hover.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1348,10 +1385,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
|||||||
child: InkWell(child: Text(label)),
|
child: InkWell(child: Text(label)),
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(
|
||||||
() => Offstage(
|
() {
|
||||||
offstage: !(tabInfo?.onTabCloseButton != null &&
|
if (tabInfo?.onTabCloseButton != null &&
|
||||||
menuHover.value),
|
menuHover.value) {
|
||||||
child: InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
tabInfo?.onTabCloseButton?.call();
|
tabInfo?.onTabCloseButton?.call();
|
||||||
if (Navigator.of(context).canPop()) {
|
if (Navigator.of(context).canPop()) {
|
||||||
@@ -1366,9 +1403,12 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
|||||||
setState(() => btnHover.value = false),
|
setState(() => btnHover.value = false),
|
||||||
child: Icon(Icons.close,
|
child: Icon(Icons.close,
|
||||||
color:
|
color:
|
||||||
btnHover.value ? Colors.red : null))),
|
btnHover.value ? Colors.red : null)));
|
||||||
|
} else {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1380,6 +1420,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _showTabBarBottomDivider(DesktopTabType tabType) {
|
||||||
|
return tabType == DesktopTabType.main || tabType == DesktopTabType.install;
|
||||||
|
}
|
||||||
|
|
||||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||||
final Color? selectedTabIconColor;
|
final Color? selectedTabIconColor;
|
||||||
final Color? unSelectedTabIconColor;
|
final Color? unSelectedTabIconColor;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:bot_toast/bot_toast.dart';
|
|||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/overlay.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/install_page.dart';
|
import 'package:flutter_hbb/desktop/pages/install_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
||||||
@@ -95,7 +96,9 @@ Future<void> main(List<String> args) async {
|
|||||||
desktopType = DesktopType.main;
|
desktopType = DesktopType.main;
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
windowManager.setPreventClose(true);
|
windowManager.setPreventClose(true);
|
||||||
windowManager.setMovable(false);
|
if (isMacOS) {
|
||||||
|
disableWindowMovable(kWindowId);
|
||||||
|
}
|
||||||
runMainApp(true);
|
runMainApp(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +157,7 @@ void runMobileApp() async {
|
|||||||
await initEnv(kAppTypeMain);
|
await initEnv(kAppTypeMain);
|
||||||
if (isAndroid) androidChannelInit();
|
if (isAndroid) androidChannelInit();
|
||||||
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||||
|
draggablePositions.load();
|
||||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
runApp(App());
|
runApp(App());
|
||||||
@@ -168,10 +172,13 @@ void runMultiWindow(
|
|||||||
final title = getWindowName();
|
final title = getWindowName();
|
||||||
// set prevent close to true, we handle close event manually
|
// set prevent close to true, we handle close event manually
|
||||||
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
||||||
WindowController.fromWindowId(kWindowId!).setMovable(false);
|
if (isMacOS) {
|
||||||
|
disableWindowMovable(kWindowId);
|
||||||
|
}
|
||||||
late Widget widget;
|
late Widget widget;
|
||||||
switch (appType) {
|
switch (appType) {
|
||||||
case kAppTypeDesktopRemote:
|
case kAppTypeDesktopRemote:
|
||||||
|
draggablePositions.load();
|
||||||
widget = DesktopRemoteScreen(
|
widget = DesktopRemoteScreen(
|
||||||
params: argument,
|
params: argument,
|
||||||
);
|
);
|
||||||
@@ -253,7 +260,7 @@ showCmWindow({bool isStartup = false}) async {
|
|||||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
||||||
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||||
bind.mainHideDocker();
|
bind.mainHideDock();
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
windowManager.show(),
|
windowManager.show(),
|
||||||
windowManager.focus(),
|
windowManager.focus(),
|
||||||
@@ -281,14 +288,14 @@ hideCmWindow({bool isStartup = false}) async {
|
|||||||
size: kConnectionManagerWindowSizeClosedChat);
|
size: kConnectionManagerWindowSizeClosedChat);
|
||||||
windowManager.setOpacity(0);
|
windowManager.setOpacity(0);
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||||
bind.mainHideDocker();
|
bind.mainHideDock();
|
||||||
await windowManager.minimize();
|
await windowManager.minimize();
|
||||||
await windowManager.hide();
|
await windowManager.hide();
|
||||||
_isCmReadyToShow = true;
|
_isCmReadyToShow = true;
|
||||||
} else if (_isCmReadyToShow) {
|
} else if (_isCmReadyToShow) {
|
||||||
if (await windowManager.getOpacity() != 0) {
|
if (await windowManager.getOpacity() != 0) {
|
||||||
await windowManager.setOpacity(0);
|
await windowManager.setOpacity(0);
|
||||||
bind.mainHideDocker();
|
bind.mainHideDock();
|
||||||
await windowManager.minimize();
|
await windowManager.minimize();
|
||||||
await windowManager.hide();
|
await windowManager.hide();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,19 +50,26 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
bool isPeersLoaded = false;
|
bool isPeersLoaded = false;
|
||||||
StreamSubscription? _uniLinksSubscription;
|
StreamSubscription? _uniLinksSubscription;
|
||||||
|
|
||||||
|
_ConnectionPageState() {
|
||||||
|
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||||
|
_idController.addListener(() {
|
||||||
|
_idEmpty.value = _idController.text.isEmpty;
|
||||||
|
});
|
||||||
|
Get.put<IDTextEditingController>(_idController);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
|
||||||
if (_idController.text.isEmpty) {
|
if (_idController.text.isEmpty) {
|
||||||
() async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||||
if (lastRemoteId != _idController.id) {
|
if (lastRemoteId != _idController.id) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_idController.id = lastRemoteId;
|
_idController.id = lastRemoteId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}();
|
});
|
||||||
}
|
}
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
if (!bind.isCustomClient()) {
|
if (!bind.isCustomClient()) {
|
||||||
@@ -72,11 +79,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_idController.addListener(() {
|
|
||||||
_idEmpty.value = _idController.text.isEmpty;
|
|
||||||
});
|
|
||||||
Get.put<IDTextEditingController>(_idController);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -395,7 +397,7 @@ class _WebMenuState extends State<WebMenu> {
|
|||||||
[
|
[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: "about",
|
value: "about",
|
||||||
child: Text('${translate('About')} RustDesk'),
|
child: Text(translate('About RustDesk')),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -204,16 +204,25 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
} else if (v == "folder") {
|
} else if (v == "folder") {
|
||||||
final name = TextEditingController();
|
final name = TextEditingController();
|
||||||
gFFI.dialogManager
|
String? errorText;
|
||||||
.show((setState, close, context) => CustomAlertDialog(
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
name.addListener(() {
|
||||||
|
if (errorText != null) {
|
||||||
|
setState(() {
|
||||||
|
errorText = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Create Folder")),
|
title: Text(translate("Create Folder")),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: translate(
|
labelText:
|
||||||
"Please enter the folder name"),
|
translate("Please enter the folder name"),
|
||||||
|
errorText: errorText,
|
||||||
),
|
),
|
||||||
controller: name,
|
controller: name,
|
||||||
),
|
),
|
||||||
@@ -221,19 +230,28 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton("Cancel",
|
dialogButton("Cancel",
|
||||||
onPressed: () => close(false),
|
onPressed: () => close(false), isOutline: true),
|
||||||
isOutline: true),
|
|
||||||
dialogButton("OK", onPressed: () {
|
dialogButton("OK", onPressed: () {
|
||||||
if (name.value.text.isNotEmpty) {
|
if (name.value.text.isNotEmpty) {
|
||||||
currentFileController.createDir(
|
if (!PathUtil.validName(
|
||||||
PathUtil.join(
|
name.value.text,
|
||||||
|
currentFileController
|
||||||
|
.options.value.isWindows)) {
|
||||||
|
setState(() {
|
||||||
|
errorText =
|
||||||
|
translate("Invalid folder name");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentFileController.createDir(PathUtil.join(
|
||||||
currentDir.path,
|
currentDir.path,
|
||||||
name.value.text,
|
name.value.text,
|
||||||
currentOptions.isWindows));
|
currentOptions.isWindows));
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]));
|
]);
|
||||||
|
});
|
||||||
} else if (v == "hidden") {
|
} else if (v == "hidden") {
|
||||||
currentFileController.toggleShowHidden();
|
currentFileController.toggleShowHidden();
|
||||||
}
|
}
|
||||||
@@ -497,6 +515,14 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
child: Text(translate("Properties")),
|
child: Text(translate("Properties")),
|
||||||
value: "properties",
|
value: "properties",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
),
|
||||||
|
if (!entries[index].isDrive &&
|
||||||
|
versionCmp(gFFI.ffiModel.pi.version,
|
||||||
|
"1.3.0") >=
|
||||||
|
0)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text(translate("Rename")),
|
||||||
|
value: "rename",
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@@ -509,6 +535,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
_selectedItems.clear();
|
_selectedItems.clear();
|
||||||
widget.selectMode.toggle(isLocal);
|
widget.selectMode.toggle(isLocal);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
} else if (v == "rename") {
|
||||||
|
controller.renameAction(
|
||||||
|
entries[index], isLocal);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class RemotePage extends StatefulWidget {
|
|||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RemotePage> createState() => _RemotePageState();
|
State<RemotePage> createState() => _RemotePageState(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RemotePageState extends State<RemotePage> {
|
class _RemotePageState extends State<RemotePage> {
|
||||||
@@ -55,6 +55,15 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
InputModel get inputModel => gFFI.inputModel;
|
InputModel get inputModel => gFFI.inputModel;
|
||||||
SessionID get sessionId => gFFI.sessionId;
|
SessionID get sessionId => gFFI.sessionId;
|
||||||
|
|
||||||
|
final TextEditingController _textController =
|
||||||
|
TextEditingController(text: initText);
|
||||||
|
|
||||||
|
_RemotePageState(String id) {
|
||||||
|
initSharedStates(id);
|
||||||
|
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
|
gFFI.dialogManager.loadMobileActionsOverlayVisible();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -77,10 +86,8 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
keyboardSubscription =
|
keyboardSubscription =
|
||||||
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
||||||
initSharedStates(widget.id);
|
|
||||||
gFFI.chatModel
|
gFFI.chatModel
|
||||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||||
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
|
||||||
_blockableOverlayState.applyFfi(gFFI);
|
_blockableOverlayState.applyFfi(gFFI);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +95,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
gFFI.dialogManager.hideMobileActionsOverlay();
|
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
|
||||||
gFFI.inputModel.listenToMouse(false);
|
gFFI.inputModel.listenToMouse(false);
|
||||||
gFFI.imageModel.disposeImage();
|
gFFI.imageModel.disposeImage();
|
||||||
gFFI.cursorModel.disposeImages();
|
gFFI.cursorModel.disposeImages();
|
||||||
@@ -105,11 +112,10 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
await keyboardSubscription.cancel();
|
await keyboardSubscription.cancel();
|
||||||
removeSharedStates(widget.id);
|
removeSharedStates(widget.id);
|
||||||
if (isAndroid) {
|
// `on_voice_call_closed` should be called when the connection is ended.
|
||||||
|
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
|
||||||
// Only one client is considered here for now.
|
// Only one client is considered here for now.
|
||||||
// TODO: take into account the case where there are multiple clients
|
gFFI.chatModel.onVoiceCallClosed("End connetion");
|
||||||
gFFI.invokeMethod("on_voice_call_closed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||||
@@ -145,37 +151,59 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle mobile virtual keyboard
|
void _handleIOSSoftKeyboardInput(String newValue) {
|
||||||
void handleSoftKeyboardInput(String newValue) {
|
|
||||||
var oldValue = _value;
|
var oldValue = _value;
|
||||||
_value = newValue;
|
_value = newValue;
|
||||||
if (isIOS) {
|
|
||||||
var i = newValue.length - 1;
|
var i = newValue.length - 1;
|
||||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
||||||
var j = oldValue.length - 1;
|
var j = oldValue.length - 1;
|
||||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
||||||
if (i < j) j = i;
|
if (i < j) j = i;
|
||||||
newValue = newValue.substring(j + 1);
|
var subNewValue = newValue.substring(j + 1);
|
||||||
oldValue = oldValue.substring(j + 1);
|
var subOldValue = oldValue.substring(j + 1);
|
||||||
|
|
||||||
|
// get common prefix of subNewValue and subOldValue
|
||||||
var common = 0;
|
var common = 0;
|
||||||
for (;
|
for (;
|
||||||
common < oldValue.length &&
|
common < subOldValue.length &&
|
||||||
common < newValue.length &&
|
common < subNewValue.length &&
|
||||||
newValue[common] == oldValue[common];
|
subNewValue[common] == subOldValue[common];
|
||||||
++common) {}
|
++common) {}
|
||||||
for (i = 0; i < oldValue.length - common; ++i) {
|
|
||||||
inputModel.inputKey('VK_BACK');
|
// get newStr from subNewValue
|
||||||
}
|
var newStr = "";
|
||||||
if (newValue.length > common) {
|
if (subNewValue.length > common) {
|
||||||
var s = newValue.substring(common);
|
newStr = subNewValue.substring(common);
|
||||||
if (s.length > 1) {
|
|
||||||
bind.sessionInputString(sessionId: sessionId, value: s);
|
|
||||||
} else {
|
|
||||||
inputChar(s);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the value to the old value and early return if is still composing. (1 && 2)
|
||||||
|
// 1. The composing range is valid
|
||||||
|
// 2. The new string is shorter than the composing range.
|
||||||
|
if (_textController.value.isComposingRangeValid) {
|
||||||
|
final composingLength = _textController.value.composing.end -
|
||||||
|
_textController.value.composing.start;
|
||||||
|
if (composingLength > newStr.length) {
|
||||||
|
_value = oldValue;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the different part in the old value.
|
||||||
|
for (i = 0; i < subOldValue.length - common; ++i) {
|
||||||
|
inputModel.inputKey('VK_BACK');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input the new string.
|
||||||
|
if (newStr.length > 1) {
|
||||||
|
bind.sessionInputString(sessionId: sessionId, value: newStr);
|
||||||
|
} else {
|
||||||
|
inputChar(newStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleNonIOSSoftKeyboardInput(String newValue) {
|
||||||
|
var oldValue = _value;
|
||||||
|
_value = newValue;
|
||||||
if (oldValue.isNotEmpty &&
|
if (oldValue.isNotEmpty &&
|
||||||
newValue.isNotEmpty &&
|
newValue.isNotEmpty &&
|
||||||
oldValue[0] == '\1' &&
|
oldValue[0] == '\1' &&
|
||||||
@@ -214,6 +242,15 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle mobile virtual keyboard
|
||||||
|
void handleSoftKeyboardInput(String newValue) {
|
||||||
|
if (isIOS) {
|
||||||
|
_handleIOSSoftKeyboardInput(newValue);
|
||||||
|
} else {
|
||||||
|
_handleNonIOSSoftKeyboardInput(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void inputChar(String char) {
|
void inputChar(String char) {
|
||||||
if (char == '\n') {
|
if (char == '\n') {
|
||||||
char = 'VK_RETURN';
|
char = 'VK_RETURN';
|
||||||
@@ -227,6 +264,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
// destroy first, so that our _value trick can work
|
// destroy first, so that our _value trick can work
|
||||||
_value = initText;
|
_value = initText;
|
||||||
|
_textController.text = _value;
|
||||||
setState(() => _showEdit = false);
|
setState(() => _showEdit = false);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
||||||
@@ -242,12 +280,10 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
|
|
||||||
|
|
||||||
Widget _bottomWidget() => _showGestureHelp
|
Widget _bottomWidget() => _showGestureHelp
|
||||||
? getGestureHelp()
|
? getGestureHelp()
|
||||||
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
|
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
|
||||||
? getBottomAppBar(keyboard)
|
? getBottomAppBar()
|
||||||
: Offstage());
|
: Offstage());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -314,7 +350,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
return Container(
|
return Container(
|
||||||
color: kColorCanvas,
|
color: kColorCanvas,
|
||||||
child: isWebDesktop
|
child: isWebDesktop
|
||||||
? getBodyForDesktopWithListener(keyboard)
|
? getBodyForDesktopWithListener()
|
||||||
: SafeArea(
|
: SafeArea(
|
||||||
child:
|
child:
|
||||||
OrientationBuilder(builder: (ctx, orientation) {
|
OrientationBuilder(builder: (ctx, orientation) {
|
||||||
@@ -346,9 +382,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget getRawPointerAndKeyBody(Widget child) {
|
Widget getRawPointerAndKeyBody(Widget child) {
|
||||||
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
return RawPointerMouseRegion(
|
return RawPointerMouseRegion(
|
||||||
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
cursor: ffiModel.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
||||||
inputModel: inputModel,
|
inputModel: inputModel,
|
||||||
// Disable RawKeyFocusScope before the connecting is established.
|
// Disable RawKeyFocusScope before the connecting is established.
|
||||||
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
|
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
|
||||||
@@ -361,7 +397,8 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getBottomAppBar(bool keyboard) {
|
Widget getBottomAppBar() {
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
return BottomAppBar(
|
return BottomAppBar(
|
||||||
elevation: 10,
|
elevation: 10,
|
||||||
color: MyTheme.accent,
|
color: MyTheme.accent,
|
||||||
@@ -387,7 +424,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
] +
|
] +
|
||||||
(isWebDesktop
|
(isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
|
||||||
? []
|
? []
|
||||||
: gFFI.ffiModel.isPeerAndroid
|
: gFFI.ffiModel.isPeerAndroid
|
||||||
? [
|
? [
|
||||||
@@ -419,17 +456,21 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
(isWeb
|
(isWeb
|
||||||
? []
|
? []
|
||||||
: <Widget>[
|
: <Widget>[
|
||||||
IconButton(
|
futureBuilder(
|
||||||
|
future: gFFI.invokeMethod(
|
||||||
|
"get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
|
||||||
|
hasData: (isSupportVoiceCall) => IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: isAndroid
|
icon: isAndroid && isSupportVoiceCall
|
||||||
? SvgPicture.asset('assets/chat.svg',
|
? SvgPicture.asset('assets/chat.svg',
|
||||||
colorFilter: ColorFilter.mode(
|
colorFilter: ColorFilter.mode(
|
||||||
Colors.white, BlendMode.srcIn))
|
Colors.white, BlendMode.srcIn))
|
||||||
: Icon(Icons.message),
|
: Icon(Icons.message),
|
||||||
onPressed: () => isAndroid
|
onPressed: () =>
|
||||||
|
isAndroid && isSupportVoiceCall
|
||||||
? showChatOptions(widget.id)
|
? showChatOptions(widget.id)
|
||||||
: onPressedTextChat(widget.id),
|
: onPressedTextChat(widget.id),
|
||||||
)
|
))
|
||||||
]) +
|
]) +
|
||||||
[
|
[
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -479,11 +520,15 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
: TextFormField(
|
: TextFormField(
|
||||||
textInputAction: TextInputAction.newline,
|
textInputAction: TextInputAction.newline,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
// Flutter 3.16.9 Android.
|
||||||
|
// `enableSuggestions` causes secure keyboard to be shown.
|
||||||
|
// https://github.com/flutter/flutter/issues/139143
|
||||||
|
// https://github.com/flutter/flutter/issues/146540
|
||||||
|
// enableSuggestions: false,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
focusNode: _mobileFocusNode,
|
focusNode: _mobileFocusNode,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
initialValue: _value,
|
controller: _textController,
|
||||||
// trick way to make backspace work always
|
// trick way to make backspace work always
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
onChanged: handleSoftKeyboardInput,
|
onChanged: handleSoftKeyboardInput,
|
||||||
@@ -491,48 +536,84 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (showCursorPaint) {
|
if (showCursorPaint) {
|
||||||
paints.add(CursorPaint());
|
paints.add(CursorPaint(widget.id));
|
||||||
}
|
}
|
||||||
return paints;
|
return paints;
|
||||||
}()));
|
}()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getBodyForDesktopWithListener(bool keyboard) {
|
Widget getBodyForDesktopWithListener() {
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
var paints = <Widget>[ImagePaint()];
|
var paints = <Widget>[ImagePaint()];
|
||||||
if (showCursorPaint) {
|
if (showCursorPaint) {
|
||||||
final cursor = bind.sessionGetToggleOptionSync(
|
final cursor = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
if (keyboard || cursor) {
|
if (ffiModel.keyboard || cursor) {
|
||||||
paints.add(CursorPaint());
|
paints.add(CursorPaint(widget.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor, child: Stack(children: paints));
|
color: MyTheme.canvasColor, child: Stack(children: paints));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<TTextMenu> _getMobileActionMenus() {
|
||||||
|
if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
|
||||||
|
!gFFI.ffiModel.keyboard) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
|
||||||
|
if (!enabled) return [];
|
||||||
|
return [
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Back')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileBack(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Home')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileHome(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Apps')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileApps(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Volume up')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Volume down')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Power')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobilePower(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void showActions(String id) async {
|
void showActions(String id) async {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final x = 120.0;
|
final x = 120.0;
|
||||||
final y = size.height;
|
final y = size.height;
|
||||||
|
final mobileActionMenus = _getMobileActionMenus();
|
||||||
final menus = toolbarControls(context, id, gFFI);
|
final menus = toolbarControls(context, id, gFFI);
|
||||||
getChild(TTextMenu menu) {
|
|
||||||
if (menu.trailingIcon != null) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
menu.child,
|
|
||||||
menu.trailingIcon!,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return menu.child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final more = menus
|
final List<PopupMenuEntry<int>> more = [
|
||||||
|
...mobileActionMenus
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
.map((e) =>
|
||||||
.toList();
|
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
|
||||||
|
.toList(),
|
||||||
|
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
|
||||||
|
...menus
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => PopupMenuItem<int>(
|
||||||
|
child: e.value.getChild(),
|
||||||
|
value: e.key + mobileActionMenus.length))
|
||||||
|
.toList(),
|
||||||
|
];
|
||||||
() async {
|
() async {
|
||||||
var index = await showMenu(
|
var index = await showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -540,8 +621,12 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
items: more,
|
items: more,
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
);
|
);
|
||||||
if (index != null && index < menus.length) {
|
if (index != null) {
|
||||||
menus[index].onPressed.call();
|
if (index < mobileActionMenus.length) {
|
||||||
|
mobileActionMenus[index].onPressed.call();
|
||||||
|
} else if (index < mobileActionMenus.length + more.length) {
|
||||||
|
menus[index - mobileActionMenus.length].onPressed.call();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
@@ -561,11 +646,13 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
child: Text(translate(label), style: labelStyle),
|
child: Text(translate(label), style: labelStyle),
|
||||||
trailingIcon: Transform.scale(
|
trailingIcon: Transform.scale(
|
||||||
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||||
|
child: IgnorePointer(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: onPressed,
|
onPressed: null,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -594,23 +681,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
),
|
),
|
||||||
onPressVoiceCall),
|
onPressVoiceCall),
|
||||||
];
|
];
|
||||||
getChild(TTextMenu menu) {
|
|
||||||
if (menu.trailingIcon != null) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
menu.child,
|
|
||||||
menu.trailingIcon!,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return menu.child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final menuItems = menus
|
final menuItems = menus
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
.map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
|
||||||
.toList();
|
.toList();
|
||||||
Future.delayed(Duration.zero, () async {
|
Future.delayed(Duration.zero, () async {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -679,6 +754,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
var _fn = false;
|
var _fn = false;
|
||||||
var _pin = false;
|
var _pin = false;
|
||||||
final _keyboardVisibilityController = KeyboardVisibilityController();
|
final _keyboardVisibilityController = KeyboardVisibilityController();
|
||||||
|
final _key = GlobalKey();
|
||||||
|
|
||||||
InputModel get inputModel => gFFI.inputModel;
|
InputModel get inputModel => gFFI.inputModel;
|
||||||
|
|
||||||
@@ -703,6 +779,19 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
onPressed: onPressed);
|
onPressed: onPressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateRect() {
|
||||||
|
RenderObject? renderObject = _key.currentContext?.findRenderObject();
|
||||||
|
if (renderObject == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (renderObject is RenderBox) {
|
||||||
|
final size = renderObject.size;
|
||||||
|
Offset pos = renderObject.localToGlobal(Offset.zero);
|
||||||
|
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
|
||||||
|
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hasModifierOn = inputModel.ctrl ||
|
final hasModifierOn = inputModel.ctrl ||
|
||||||
@@ -711,6 +800,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
inputModel.command;
|
inputModel.command;
|
||||||
|
|
||||||
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
||||||
|
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null);
|
||||||
return Offstage();
|
return Offstage();
|
||||||
}
|
}
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -821,7 +911,12 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
final space = size.width > 320 ? 4.0 : 2.0;
|
final space = size.width > 320 ? 4.0 : 2.0;
|
||||||
|
// 500 ms is long enough for this widget to be built!
|
||||||
|
Future.delayed(Duration(milliseconds: 500), () {
|
||||||
|
_updateRect();
|
||||||
|
});
|
||||||
return Container(
|
return Container(
|
||||||
|
key: _key,
|
||||||
color: Color(0xAA000000),
|
color: Color(0xAA000000),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
|
top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
|
||||||
@@ -852,26 +947,52 @@ class ImagePaint extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CursorPaint extends StatelessWidget {
|
class CursorPaint extends StatelessWidget {
|
||||||
|
late final String id;
|
||||||
|
CursorPaint(this.id);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final m = Provider.of<CursorModel>(context);
|
final m = Provider.of<CursorModel>(context);
|
||||||
final c = Provider.of<CanvasModel>(context);
|
final c = Provider.of<CanvasModel>(context);
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
||||||
var s = c.scale;
|
final s = c.scale;
|
||||||
double hotx = m.hotx;
|
double hotx = m.hotx;
|
||||||
double hoty = m.hoty;
|
double hoty = m.hoty;
|
||||||
if (m.image == null) {
|
var image = m.image;
|
||||||
|
if (image == null) {
|
||||||
if (preDefaultCursor.image != null) {
|
if (preDefaultCursor.image != null) {
|
||||||
|
image = preDefaultCursor.image;
|
||||||
hotx = preDefaultCursor.image!.width / 2;
|
hotx = preDefaultCursor.image!.width / 2;
|
||||||
hoty = preDefaultCursor.image!.height / 2;
|
hoty = preDefaultCursor.image!.height / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (preForbiddenCursor.image != null &&
|
||||||
|
!ffiModel.viewOnly &&
|
||||||
|
!ffiModel.keyboard &&
|
||||||
|
!ShowRemoteCursorState.find(id).value) {
|
||||||
|
image = preForbiddenCursor.image;
|
||||||
|
hotx = preForbiddenCursor.image!.width / 2;
|
||||||
|
hoty = preForbiddenCursor.image!.height / 2;
|
||||||
|
}
|
||||||
|
if (image == null) {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final minSize = 12.0;
|
||||||
|
double mins =
|
||||||
|
minSize / (image.width > image.height ? image.width : image.height);
|
||||||
|
double factor = 1.0;
|
||||||
|
if (s < mins) {
|
||||||
|
factor = s / mins;
|
||||||
|
}
|
||||||
|
final s2 = s < mins ? mins : s;
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: ImagePainter(
|
painter: ImagePainter(
|
||||||
image: m.image ?? preDefaultCursor.image,
|
image: image,
|
||||||
x: m.x * s - hotx + c.x,
|
x: (m.x - hotx) * factor + c.x / s2,
|
||||||
y: m.y * s - hoty + c.y - adjust,
|
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
|
||||||
scale: 1),
|
scale: s2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -901,7 +1022,7 @@ void showOptions(
|
|||||||
border: Border.all(color: Theme.of(context).hintColor),
|
border: Border.all(color: Theme.of(context).hintColor),
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
color: i == cur
|
color: i == cur
|
||||||
? Theme.of(context).toggleableActiveColor.withOpacity(0.6)
|
? Theme.of(context).primaryColor.withOpacity(0.6)
|
||||||
: null),
|
: null),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text((i + 1).toString(),
|
child: Text((i + 1).toString(),
|
||||||
@@ -949,22 +1070,40 @@ void showOptions(
|
|||||||
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
||||||
final radios = [
|
final radios = [
|
||||||
for (var e in viewStyleRadios)
|
for (var e in viewStyleRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
|
e.child,
|
||||||
|
e.value,
|
||||||
|
viewStyle.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
e.onChanged?.call(v);
|
e.onChanged?.call(v);
|
||||||
if (v != null) viewStyle.value = v;
|
if (v != null) viewStyle.value = v;
|
||||||
})),
|
}
|
||||||
|
: null)),
|
||||||
const Divider(color: MyTheme.border),
|
const Divider(color: MyTheme.border),
|
||||||
for (var e in imageQualityRadios)
|
for (var e in imageQualityRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
|
e.child,
|
||||||
|
e.value,
|
||||||
|
imageQuality.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
e.onChanged?.call(v);
|
e.onChanged?.call(v);
|
||||||
if (v != null) imageQuality.value = v;
|
if (v != null) imageQuality.value = v;
|
||||||
})),
|
}
|
||||||
|
: null)),
|
||||||
const Divider(color: MyTheme.border),
|
const Divider(color: MyTheme.border),
|
||||||
for (var e in codecRadios)
|
for (var e in codecRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
|
e.child,
|
||||||
|
e.value,
|
||||||
|
codec.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
e.onChanged?.call(v);
|
e.onChanged?.call(v);
|
||||||
if (v != null) codec.value = v;
|
if (v != null) codec.value = v;
|
||||||
})),
|
}
|
||||||
|
: null)),
|
||||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||||
];
|
];
|
||||||
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
||||||
@@ -975,10 +1114,12 @@ void showOptions(
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
value: rxCursorToggleValues[e.key].value,
|
value: rxCursorToggleValues[e.key].value,
|
||||||
onChanged: (v) {
|
onChanged: e.value.onChanged != null
|
||||||
|
? (v) {
|
||||||
e.value.onChanged?.call(v);
|
e.value.onChanged?.call(v);
|
||||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
title: e.value.child)))
|
title: e.value.child)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -990,10 +1131,12 @@ void showOptions(
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
value: rxToggleValues[e.key].value,
|
value: rxToggleValues[e.key].value,
|
||||||
onChanged: (v) {
|
onChanged: e.value.onChanged != null
|
||||||
|
? (v) {
|
||||||
e.value.onChanged?.call(v);
|
e.value.onChanged?.call(v);
|
||||||
if (v != null) rxToggleValues[e.key].value = v;
|
if (v != null) rxToggleValues[e.key].value = v;
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
title: e.value.child)))
|
title: e.value.child)))
|
||||||
.toList();
|
.toList();
|
||||||
final toggles = [
|
final toggles = [
|
||||||
@@ -1013,14 +1156,110 @@ void showOptions(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var popupDialogMenus = List<Widget>.empty(growable: true);
|
||||||
|
final resolution = getResolutionMenu(gFFI, id);
|
||||||
|
if (resolution != null) {
|
||||||
|
popupDialogMenus.add(ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: resolution.child,
|
||||||
|
onTap: () {
|
||||||
|
close();
|
||||||
|
resolution.onPressed();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
final virtualDisplayMenu = getVirtualDisplayMenu(gFFI, id);
|
||||||
|
if (virtualDisplayMenu != null) {
|
||||||
|
popupDialogMenus.add(ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: virtualDisplayMenu.child,
|
||||||
|
onTap: () {
|
||||||
|
close();
|
||||||
|
virtualDisplayMenu.onPressed();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (popupDialogMenus.isNotEmpty) {
|
||||||
|
popupDialogMenus.add(const Divider(color: MyTheme.border));
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: displays + radios + toggles + [privacyModeWidget]),
|
children: displays +
|
||||||
|
radios +
|
||||||
|
popupDialogMenus +
|
||||||
|
toggles +
|
||||||
|
[privacyModeWidget]),
|
||||||
);
|
);
|
||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
||||||
|
if (!showVirtualDisplayMenu(ffi)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return TTextMenu(
|
||||||
|
child: Text(translate("Virtual display")),
|
||||||
|
onPressed: () {
|
||||||
|
ffi.dialogManager.show((setState, close, context) {
|
||||||
|
final children = getVirtualDisplayMenuChildren(ffi, id, close);
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('Virtual display')),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TTextMenu? getResolutionMenu(FFI ffi, String id) {
|
||||||
|
final ffiModel = ffi.ffiModel;
|
||||||
|
final pi = ffiModel.pi;
|
||||||
|
final resolutions = pi.resolutions;
|
||||||
|
final display = pi.tryGetDisplayIfNotAllDisplay(display: pi.currentDisplay);
|
||||||
|
|
||||||
|
final visible =
|
||||||
|
ffiModel.keyboard && (resolutions.length > 1) && display != null;
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return TTextMenu(
|
||||||
|
child: Text(translate("Resolution")),
|
||||||
|
onPressed: () {
|
||||||
|
ffi.dialogManager.show((setState, close, context) {
|
||||||
|
final children = resolutions
|
||||||
|
.map((e) => getRadio<String>(
|
||||||
|
Text('${e.width}x${e.height}'),
|
||||||
|
'${e.width}x${e.height}',
|
||||||
|
'${display.width}x${display.height}',
|
||||||
|
(value) {
|
||||||
|
close();
|
||||||
|
bind.sessionChangeResolution(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
display: pi.currentDisplay,
|
||||||
|
width: e.width,
|
||||||
|
height: e.height,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('Resolution')),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void sendPrompt(bool isMac, String key) {
|
void sendPrompt(bool isMac, String key) {
|
||||||
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -22,7 +23,22 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
final icon = const Icon(Icons.mobile_screen_share);
|
final icon = const Icon(Icons.mobile_screen_share);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final appBarActions = [
|
final appBarActions = (!bind.isDisableSettings() &&
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
||||||
|
? [_DropDownAction()]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
ServerPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ServerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DropDownAction extends StatelessWidget {
|
||||||
|
_DropDownAction();
|
||||||
|
|
||||||
|
// should only have one action
|
||||||
|
final actions = [
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
tooltip: "",
|
tooltip: "",
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
@@ -101,18 +117,27 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onSelected: (value) {
|
onSelected: (value) async {
|
||||||
if (value == "changeID") {
|
if (value == "changeID") {
|
||||||
changeIdDialog();
|
changeIdDialog();
|
||||||
} else if (value == "setPermanentPassword") {
|
} else if (value == "setPermanentPassword") {
|
||||||
setPermanentPasswordDialog(gFFI.dialogManager);
|
setPasswordDialog();
|
||||||
} else if (value == "setTemporaryPasswordLength") {
|
} else if (value == "setTemporaryPasswordLength") {
|
||||||
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
||||||
} else if (value == kUsePermanentPassword ||
|
} else if (value == kUsePermanentPassword ||
|
||||||
value == kUseTemporaryPassword ||
|
value == kUseTemporaryPassword ||
|
||||||
value == kUseBothPasswords) {
|
value == kUseBothPasswords) {
|
||||||
|
callback() {
|
||||||
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
||||||
gFFI.serverModel.updatePasswordModel();
|
gFFI.serverModel.updatePasswordModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == kUsePermanentPassword &&
|
||||||
|
(await bind.mainGetPermanentPassword()).isEmpty) {
|
||||||
|
setPasswordDialog(notEmptyCallback: callback);
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
} else if (value.startsWith("AcceptSessionsVia")) {
|
} else if (value.startsWith("AcceptSessionsVia")) {
|
||||||
value = value.substring("AcceptSessionsVia".length);
|
value = value.substring("AcceptSessionsVia".length);
|
||||||
if (value == "Password") {
|
if (value == "Password") {
|
||||||
@@ -126,10 +151,10 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
ServerPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _ServerPageState();
|
Widget build(BuildContext context) {
|
||||||
|
return actions[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerPageState extends State<ServerPage> {
|
class _ServerPageState extends State<ServerPage> {
|
||||||
@@ -162,7 +187,7 @@ class _ServerPageState extends State<ServerPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
buildPresetPasswordWarning(),
|
buildPresetPasswordWarningMobile(),
|
||||||
gFFI.serverModel.isStart
|
gFFI.serverModel.isStart
|
||||||
? ServerInfo()
|
? ServerInfo()
|
||||||
: ServiceNotRunningNotification(),
|
: ServiceNotRunningNotification(),
|
||||||
@@ -854,6 +879,15 @@ void androidChannelInit() {
|
|||||||
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
|
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "stop_service":
|
||||||
|
{
|
||||||
|
print(
|
||||||
|
"stop_service by kotlin, isStart:${gFFI.serverModel.isStart}");
|
||||||
|
if (gFFI.serverModel.isStart) {
|
||||||
|
gFFI.serverModel.stopService();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(label: "MethodCallHandler err:$e");
|
debugPrintStack(label: "MethodCallHandler err:$e");
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
import 'package:settings_ui/settings_ui.dart';
|
||||||
@@ -35,10 +37,41 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
|||||||
|
|
||||||
const url = 'https://rustdesk.com/';
|
const url = 'https://rustdesk.com/';
|
||||||
|
|
||||||
|
enum KeepScreenOn {
|
||||||
|
never,
|
||||||
|
duringControlled,
|
||||||
|
serviceOn,
|
||||||
|
}
|
||||||
|
|
||||||
|
String _keepScreenOnToOption(KeepScreenOn value) {
|
||||||
|
switch (value) {
|
||||||
|
case KeepScreenOn.never:
|
||||||
|
return 'never';
|
||||||
|
case KeepScreenOn.duringControlled:
|
||||||
|
return 'during-controlled';
|
||||||
|
case KeepScreenOn.serviceOn:
|
||||||
|
return 'service-on';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeepScreenOn optionToKeepScreenOn(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'never':
|
||||||
|
return KeepScreenOn.never;
|
||||||
|
case 'service-on':
|
||||||
|
return KeepScreenOn.serviceOn;
|
||||||
|
default:
|
||||||
|
return KeepScreenOn.duringControlled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||||
final _hasIgnoreBattery = androidVersion >= 26;
|
final _hasIgnoreBattery =
|
||||||
|
false; //androidVersion >= 26; // remove because not work on every device
|
||||||
var _ignoreBatteryOpt = false;
|
var _ignoreBatteryOpt = false;
|
||||||
var _enableStartOnBoot = false;
|
var _enableStartOnBoot = false;
|
||||||
|
var _floatingWindowDisabled = false;
|
||||||
|
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
|
||||||
var _enableAbr = false;
|
var _enableAbr = false;
|
||||||
var _denyLANDiscovery = false;
|
var _denyLANDiscovery = false;
|
||||||
var _onlyWhiteList = false;
|
var _onlyWhiteList = false;
|
||||||
@@ -52,13 +85,45 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _fingerprint = "";
|
var _fingerprint = "";
|
||||||
var _buildDate = "";
|
var _buildDate = "";
|
||||||
var _autoDisconnectTimeout = "";
|
var _autoDisconnectTimeout = "";
|
||||||
|
var _hideServer = false;
|
||||||
|
var _hideProxy = false;
|
||||||
|
var _hideNetwork = false;
|
||||||
|
var _enableTrustedDevices = false;
|
||||||
|
|
||||||
|
_SettingsState() {
|
||||||
|
_enableAbr = option2bool(
|
||||||
|
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
||||||
|
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
|
||||||
|
_onlyWhiteList = whitelistNotEmpty();
|
||||||
|
_enableDirectIPAccess = option2bool(
|
||||||
|
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
|
||||||
|
_enableRecordSession = option2bool(kOptionEnableRecordSession,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableRecordSession));
|
||||||
|
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
||||||
|
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||||
|
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||||
|
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||||
|
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
||||||
|
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
||||||
|
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||||
|
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
|
||||||
|
_autoDisconnectTimeout =
|
||||||
|
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
|
||||||
|
_hideServer =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||||
|
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||||
|
_hideNetwork =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
||||||
|
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
() async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
var update = false;
|
var update = false;
|
||||||
|
|
||||||
if (_hasIgnoreBattery) {
|
if (_hasIgnoreBattery) {
|
||||||
@@ -86,67 +151,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_enableStartOnBoot = enableStartOnBoot;
|
_enableStartOnBoot = enableStartOnBoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableAbrRes = option2bool(
|
var floatingWindowDisabled =
|
||||||
kOptionEnableAbr, await bind.mainGetOption(key: kOptionEnableAbr));
|
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
||||||
if (enableAbrRes != _enableAbr) {
|
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
||||||
|
if (floatingWindowDisabled != _floatingWindowDisabled) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableAbr = enableAbrRes;
|
_floatingWindowDisabled = floatingWindowDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
final denyLanDiscovery = !option2bool('enable-lan-discovery',
|
final keepScreenOn = _floatingWindowDisabled
|
||||||
await bind.mainGetOption(key: 'enable-lan-discovery'));
|
? KeepScreenOn.never
|
||||||
if (denyLanDiscovery != _denyLANDiscovery) {
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
|
||||||
|
if (keepScreenOn != _keepScreenOn) {
|
||||||
update = true;
|
update = true;
|
||||||
_denyLANDiscovery = denyLanDiscovery;
|
_keepScreenOn = keepScreenOn;
|
||||||
}
|
|
||||||
|
|
||||||
final onlyWhiteList = (await bind.mainGetOption(key: kOptionWhitelist)) !=
|
|
||||||
defaultOptionWhitelist;
|
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
|
||||||
update = true;
|
|
||||||
_onlyWhiteList = onlyWhiteList;
|
|
||||||
}
|
|
||||||
|
|
||||||
final enableDirectIPAccess = option2bool(kOptionDirectServer,
|
|
||||||
await bind.mainGetOption(key: kOptionDirectServer));
|
|
||||||
if (enableDirectIPAccess != _enableDirectIPAccess) {
|
|
||||||
update = true;
|
|
||||||
_enableDirectIPAccess = enableDirectIPAccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
final enableRecordSession = option2bool(kOptionEnableRecordSession,
|
|
||||||
await bind.mainGetOption(key: kOptionEnableRecordSession));
|
|
||||||
if (enableRecordSession != _enableRecordSession) {
|
|
||||||
update = true;
|
|
||||||
_enableRecordSession = enableRecordSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
final enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
|
||||||
await bind.mainGetOption(key: kOptionEnableHwcodec));
|
|
||||||
if (_enableHardwareCodec != enableHardwareCodec) {
|
|
||||||
update = true;
|
|
||||||
_enableHardwareCodec = enableHardwareCodec;
|
|
||||||
}
|
|
||||||
|
|
||||||
final autoRecordIncomingSession = option2bool(
|
|
||||||
kOptionAllowAutoRecordIncoming,
|
|
||||||
await bind.mainGetOption(key: kOptionAllowAutoRecordIncoming));
|
|
||||||
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
|
|
||||||
update = true;
|
|
||||||
_autoRecordIncomingSession = autoRecordIncomingSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
final localIP = await bind.mainGetOption(key: 'local-ip-addr');
|
|
||||||
if (localIP != _localIP) {
|
|
||||||
update = true;
|
|
||||||
_localIP = localIP;
|
|
||||||
}
|
|
||||||
|
|
||||||
final directAccessPort =
|
|
||||||
await bind.mainGetOption(key: kOptionDirectAccessPort);
|
|
||||||
if (directAccessPort != _directAccessPort) {
|
|
||||||
update = true;
|
|
||||||
_directAccessPort = directAccessPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final fingerprint = await bind.mainGetFingerprint();
|
final fingerprint = await bind.mainGetFingerprint();
|
||||||
@@ -160,25 +179,10 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
update = true;
|
update = true;
|
||||||
_buildDate = buildDate;
|
_buildDate = buildDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
final allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
|
||||||
await bind.mainGetOption(key: kOptionAllowAutoDisconnect));
|
|
||||||
if (allowAutoDisconnect != _allowAutoDisconnect) {
|
|
||||||
update = true;
|
|
||||||
_allowAutoDisconnect = allowAutoDisconnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
final autoDisconnectTimeout =
|
|
||||||
await bind.mainGetOption(key: kOptionAutoDisconnectTimeout);
|
|
||||||
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
|
|
||||||
update = true;
|
|
||||||
_autoDisconnectTimeout = autoDisconnectTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (update) {
|
if (update) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -242,18 +246,76 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
],
|
],
|
||||||
));
|
));
|
||||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
final enable2fa = bind.mainHasValid2FaSync();
|
||||||
|
final List<AbstractSettingsTile> tfaTiles = [
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('enable-2fa-title')),
|
title: Text(translate('enable-2fa-title')),
|
||||||
initialValue: bind.mainHasValid2FaSync(),
|
initialValue: enable2fa,
|
||||||
onToggle: (_) async {
|
onToggle: (v) async {
|
||||||
update() async {
|
update() async {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (v == false) {
|
||||||
|
CommonConfirmDialog(
|
||||||
|
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
||||||
change2fa(callback: update);
|
change2fa(callback: update);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
change2fa(callback: update);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (enable2fa)
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title: Text(translate('Telegram bot')),
|
||||||
|
initialValue: bind.mainHasValidBotSync(),
|
||||||
|
onToggle: (v) async {
|
||||||
|
update() async {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v == false) {
|
||||||
|
CommonConfirmDialog(
|
||||||
|
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
|
||||||
|
changeBot(callback: update);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
changeBot(callback: update);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (enable2fa)
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(translate('Enable trusted devices')),
|
||||||
|
Text('* ${translate('enable-trusted-devices-tip')}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
initialValue: _enableTrustedDevices,
|
||||||
|
onToggle: isOptionFixed(kOptionEnableTrustedDevices)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
|
mainSetBoolOption(kOptionEnableTrustedDevices, v);
|
||||||
|
setState(() {
|
||||||
|
_enableTrustedDevices = v;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (enable2fa && _enableTrustedDevices)
|
||||||
|
SettingsTile(
|
||||||
|
title: Text(translate('Manage trusted devices')),
|
||||||
|
trailing: Icon(Icons.arrow_forward_ios),
|
||||||
|
onPressed: (context) {
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||||
|
return _ManageTrustedDevices();
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
];
|
||||||
|
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Deny LAN discovery')),
|
title: Text(translate('Deny LAN discovery')),
|
||||||
initialValue: _denyLANDiscovery,
|
initialValue: _denyLANDiscovery,
|
||||||
@@ -282,9 +344,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
initialValue: _onlyWhiteList,
|
initialValue: _onlyWhiteList,
|
||||||
onToggle: (_) async {
|
onToggle: (_) async {
|
||||||
update() async {
|
update() async {
|
||||||
final onlyWhiteList =
|
final onlyWhiteList = whitelistNotEmpty();
|
||||||
(await bind.mainGetOption(key: kOptionWhitelist)) !=
|
|
||||||
defaultOptionWhitelist;
|
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
if (onlyWhiteList != _onlyWhiteList) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onlyWhiteList = onlyWhiteList;
|
_onlyWhiteList = onlyWhiteList;
|
||||||
@@ -301,10 +361,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: isOptionFixed(kOptionEnableAbr)
|
onToggle: isOptionFixed(kOptionEnableAbr)
|
||||||
? null
|
? null
|
||||||
: (v) async {
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await mainSetBoolOption(kOptionEnableAbr, v);
|
||||||
key: kOptionEnableAbr, value: v ? defaultOptionYes : "N");
|
final newValue = await mainGetBoolOption(kOptionEnableAbr);
|
||||||
final newValue =
|
|
||||||
await bind.mainGetOption(key: kOptionEnableAbr) != "N";
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_enableAbr = newValue;
|
_enableAbr = newValue;
|
||||||
});
|
});
|
||||||
@@ -316,12 +374,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
||||||
? null
|
? null
|
||||||
: (v) async {
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await mainSetBoolOption(kOptionEnableRecordSession, v);
|
||||||
key: kOptionEnableRecordSession,
|
|
||||||
value: v ? defaultOptionYes : "N");
|
|
||||||
final newValue =
|
final newValue =
|
||||||
await bind.mainGetOption(key: kOptionEnableRecordSession) !=
|
await mainGetBoolOption(kOptionEnableRecordSession);
|
||||||
"N";
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_enableRecordSession = newValue;
|
_enableRecordSession = newValue;
|
||||||
});
|
});
|
||||||
@@ -493,7 +548,59 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
onFloatingWindowChanged(bool toValue) async {
|
||||||
|
if (toValue) {
|
||||||
|
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||||
|
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final disable = !toValue;
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: kOptionDisableFloatingWindow,
|
||||||
|
value: disable ? 'Y' : defaultOptionNo);
|
||||||
|
setState(() => _floatingWindowDisabled = disable);
|
||||||
|
gFFI.serverModel.androidUpdatekeepScreenOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
enhancementsTiles.add(SettingsTile.switchTile(
|
||||||
|
initialValue: !_floatingWindowDisabled,
|
||||||
|
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(translate('Floating window')),
|
||||||
|
Text('* ${translate('floating_window_tip')}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
]),
|
||||||
|
onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
|
||||||
|
? null
|
||||||
|
: onFloatingWindowChanged));
|
||||||
|
|
||||||
|
enhancementsTiles.add(_getPopupDialogRadioEntry(
|
||||||
|
title: 'Keep screen on',
|
||||||
|
list: [
|
||||||
|
_RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
|
||||||
|
_RadioEntry('During controlled',
|
||||||
|
_keepScreenOnToOption(KeepScreenOn.duringControlled)),
|
||||||
|
_RadioEntry('During service is on',
|
||||||
|
_keepScreenOnToOption(KeepScreenOn.serviceOn)),
|
||||||
|
],
|
||||||
|
getter: () => _keepScreenOnToOption(_floatingWindowDisabled
|
||||||
|
? KeepScreenOn.never
|
||||||
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
|
||||||
|
asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
await bind.mainSetLocalOption(
|
||||||
|
key: kOptionKeepScreenOn, value: value);
|
||||||
|
setState(() => _keepScreenOn = optionToKeepScreenOn(value));
|
||||||
|
gFFI.serverModel.androidUpdatekeepScreenOn();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
final disabledSettings = bind.isDisableSettings();
|
final disabledSettings = bind.isDisableSettings();
|
||||||
|
final hideSecuritySettings =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
|
||||||
final settings = SettingsList(
|
final settings = SettingsList(
|
||||||
sections: [
|
sections: [
|
||||||
customClientSection,
|
customClientSection,
|
||||||
@@ -517,13 +624,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||||
if (!disabledSettings)
|
if (!disabledSettings && !_hideNetwork && !_hideServer)
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate('ID/Relay Server')),
|
title: Text(translate('ID/Relay Server')),
|
||||||
leading: Icon(Icons.cloud),
|
leading: Icon(Icons.cloud),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showServerSettings(gFFI.dialogManager);
|
showServerSettings(gFFI.dialogManager);
|
||||||
}),
|
}),
|
||||||
|
if (!isIOS && !_hideNetwork && !_hideProxy)
|
||||||
|
SettingsTile(
|
||||||
|
title: Text(translate('Socks5/Http(s) Proxy')),
|
||||||
|
leading: Icon(Icons.network_ping),
|
||||||
|
onPressed: (context) {
|
||||||
|
changeSocks5Proxy();
|
||||||
|
}),
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate('Language')),
|
title: Text(translate('Language')),
|
||||||
leading: Icon(Icons.translate),
|
leading: Icon(Icons.translate),
|
||||||
@@ -533,8 +647,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate(
|
title: Text(translate(
|
||||||
Theme.of(context).brightness == Brightness.light
|
Theme.of(context).brightness == Brightness.light
|
||||||
? 'Dark Theme'
|
? 'Light Theme'
|
||||||
: 'Light Theme')),
|
: 'Dark Theme')),
|
||||||
leading: Icon(Theme.of(context).brightness == Brightness.light
|
leading: Icon(Theme.of(context).brightness == Brightness.light
|
||||||
? Icons.dark_mode
|
? Icons.dark_mode
|
||||||
: Icons.light_mode),
|
: Icons.light_mode),
|
||||||
@@ -551,12 +665,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: isOptionFixed(kOptionEnableHwcodec)
|
onToggle: isOptionFixed(kOptionEnableHwcodec)
|
||||||
? null
|
? null
|
||||||
: (v) async {
|
: (v) async {
|
||||||
await bind.mainSetOption(
|
await mainSetBoolOption(kOptionEnableHwcodec, v);
|
||||||
key: kOptionEnableHwcodec,
|
|
||||||
value: v ? defaultOptionYes : "N");
|
|
||||||
final newValue =
|
final newValue =
|
||||||
await bind.mainGetOption(key: kOptionEnableHwcodec) !=
|
await mainGetBoolOption(kOptionEnableHwcodec);
|
||||||
"N";
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_enableHardwareCodec = newValue;
|
_enableHardwareCodec = newValue;
|
||||||
});
|
});
|
||||||
@@ -571,11 +682,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
title:
|
title:
|
||||||
Text(translate('Automatically record incoming sessions')),
|
Text(translate('Automatically record incoming sessions')),
|
||||||
leading: Icon(Icons.videocam),
|
leading: Icon(Icons.videocam),
|
||||||
description: FutureBuilder(
|
description: Text(
|
||||||
builder: (ctx, data) => Offstage(
|
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
|
||||||
offstage: !data.hasData,
|
|
||||||
child: Text("${translate("Directory")}: ${data.data}")),
|
|
||||||
future: bind.mainVideoSaveDirectory(root: false)),
|
|
||||||
initialValue: _autoRecordIncomingSession,
|
initialValue: _autoRecordIncomingSession,
|
||||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||||
? null
|
? null
|
||||||
@@ -595,13 +703,24 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
if (isAndroid &&
|
||||||
|
!disabledSettings &&
|
||||||
|
!outgoingOnly &&
|
||||||
|
!hideSecuritySettings)
|
||||||
|
SettingsSection(title: Text('2FA'), tiles: tfaTiles),
|
||||||
|
if (isAndroid &&
|
||||||
|
!disabledSettings &&
|
||||||
|
!outgoingOnly &&
|
||||||
|
!hideSecuritySettings)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Share Screen")),
|
title: Text(translate("Share Screen")),
|
||||||
tiles: shareScreenTiles,
|
tiles: shareScreenTiles,
|
||||||
),
|
),
|
||||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
if (isAndroid &&
|
||||||
|
!disabledSettings &&
|
||||||
|
!outgoingOnly &&
|
||||||
|
!hideSecuritySettings)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Enhancements")),
|
title: Text(translate("Enhancements")),
|
||||||
tiles: enhancementsTiles,
|
tiles: enhancementsTiles,
|
||||||
@@ -756,7 +875,7 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
|||||||
void showAbout(OverlayDialogManager dialogManager) {
|
void showAbout(OverlayDialogManager dialogManager) {
|
||||||
dialogManager.show((setState, close, context) {
|
dialogManager.show((setState, close, context) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text('${translate('About')} RustDesk'),
|
title: Text(translate('About RustDesk')),
|
||||||
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
||||||
Text('Version: $version'),
|
Text('Version: $version'),
|
||||||
InkWell(
|
InkWell(
|
||||||
@@ -910,6 +1029,51 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ManageTrustedDevices extends StatefulWidget {
|
||||||
|
const _ManageTrustedDevices();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> {
|
||||||
|
RxList<TrustedDevice> trustedDevices = RxList.empty(growable: true);
|
||||||
|
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(translate('Manage trusted devices')),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
Obx(() => IconButton(
|
||||||
|
icon: Icon(Icons.delete, color: Colors.white),
|
||||||
|
onPressed: selectedDevices.isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
confrimDeleteTrustedDevicesDialog(
|
||||||
|
trustedDevices, selectedDevices);
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: FutureBuilder(
|
||||||
|
future: TrustedDevice.get(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('Error: ${snapshot.error}'));
|
||||||
|
}
|
||||||
|
final devices = snapshot.data as List<TrustedDevice>;
|
||||||
|
trustedDevices = devices.obs;
|
||||||
|
return trustedDevicesTable(trustedDevices, selectedDevices);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _RadioEntry {
|
class _RadioEntry {
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
@@ -919,7 +1083,7 @@ class _RadioEntry {
|
|||||||
typedef _RadioEntryGetter = String Function();
|
typedef _RadioEntryGetter = String Function();
|
||||||
typedef _RadioEntrySetter = Future<void> Function(String);
|
typedef _RadioEntrySetter = Future<void> Function(String);
|
||||||
|
|
||||||
_getPopupDialogRadioEntry({
|
SettingsTile _getPopupDialogRadioEntry({
|
||||||
required String title,
|
required String title,
|
||||||
required List<_RadioEntry> list,
|
required List<_RadioEntry> list,
|
||||||
required _RadioEntryGetter getter,
|
required _RadioEntryGetter getter,
|
||||||
@@ -973,7 +1137,7 @@ _getPopupDialogRadioEntry({
|
|||||||
|
|
||||||
return SettingsTile(
|
return SettingsTile(
|
||||||
title: Text(translate(title)),
|
title: Text(translate(title)),
|
||||||
onPressed: (context) => showDialog(),
|
onPressed: asyncSetter == null ? null : (context) => showDialog(),
|
||||||
value: Padding(
|
value: Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Obx(() => Text(translate(valueText.value))),
|
child: Obx(() => Text(translate(valueText.value))),
|
||||||
|
|||||||
@@ -41,18 +41,16 @@ class GestureHelp extends StatefulWidget {
|
|||||||
final OnTouchModeChange onTouchModeChange;
|
final OnTouchModeChange onTouchModeChange;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _GestureHelpState();
|
State<StatefulWidget> createState() => _GestureHelpState(touchMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GestureHelpState extends State<GestureHelp> {
|
class _GestureHelpState extends State<GestureHelp> {
|
||||||
var _selectedIndex;
|
late int _selectedIndex;
|
||||||
var _touchMode;
|
late bool _touchMode;
|
||||||
|
|
||||||
@override
|
_GestureHelpState(bool touchMode) {
|
||||||
void initState() {
|
_touchMode = touchMode;
|
||||||
_touchMode = widget.touchMode;
|
|
||||||
_selectedIndex = _touchMode ? 1 : 0;
|
_selectedIndex = _touchMode ? 1 : 0;
|
||||||
super.initState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ import '../common.dart';
|
|||||||
|
|
||||||
final syncAbOption = 'sync-ab-with-recent-sessions';
|
final syncAbOption = 'sync-ab-with-recent-sessions';
|
||||||
bool shouldSyncAb() {
|
bool shouldSyncAb() {
|
||||||
return bind.mainGetLocalOption(key: syncAbOption).isNotEmpty;
|
return bind.mainGetLocalOption(key: syncAbOption) == 'Y';
|
||||||
}
|
}
|
||||||
|
|
||||||
final sortAbTagsOption = 'sync-ab-tags';
|
final sortAbTagsOption = 'sync-ab-tags';
|
||||||
bool shouldSortTags() {
|
bool shouldSortTags() {
|
||||||
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
|
return bind.mainGetLocalOption(key: sortAbTagsOption) == 'Y';
|
||||||
}
|
}
|
||||||
|
|
||||||
final filterAbTagOption = 'filter-ab-by-intersection';
|
final filterAbTagOption = 'filter-ab-by-intersection';
|
||||||
bool filterAbTagByIntersection() {
|
bool filterAbTagByIntersection() {
|
||||||
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty;
|
return bind.mainGetLocalOption(key: filterAbTagOption) == 'Y';
|
||||||
}
|
}
|
||||||
|
|
||||||
const _personalAddressBookName = "My address book";
|
const _personalAddressBookName = "My address book";
|
||||||
@@ -111,9 +111,10 @@ class AbModel {
|
|||||||
Future<void> _pullAb(
|
Future<void> _pullAb(
|
||||||
{required ForcePullAb? force, required bool quiet}) async {
|
{required ForcePullAb? force, required bool quiet}) async {
|
||||||
if (bind.isDisableAb()) return;
|
if (bind.isDisableAb()) return;
|
||||||
debugPrint("pullAb, force: $force, quiet: $quiet");
|
|
||||||
if (!gFFI.userModel.isLogin) return;
|
if (!gFFI.userModel.isLogin) return;
|
||||||
|
if (gFFI.userModel.networkError.isNotEmpty) return;
|
||||||
if (force == null && listInitialized && current.initialized) return;
|
if (force == null && listInitialized && current.initialized) return;
|
||||||
|
debugPrint("pullAb, force: $force, quiet: $quiet");
|
||||||
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
|
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
|
||||||
try {
|
try {
|
||||||
// Read personal guid every time to avoid upgrading the server without closing the main window
|
// Read personal guid every time to avoid upgrading the server without closing the main window
|
||||||
@@ -815,8 +816,6 @@ abstract class BaseAb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LegacyAb extends BaseAb {
|
class LegacyAb extends BaseAb {
|
||||||
final sortTags = shouldSortTags().obs;
|
|
||||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
|
||||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||||
// licensedDevices is obtained from personal ab, shared ab restrict it in server
|
// licensedDevices is obtained from personal ab, shared ab restrict it in server
|
||||||
var licensedDevices = 0;
|
var licensedDevices = 0;
|
||||||
@@ -1209,8 +1208,6 @@ class LegacyAb extends BaseAb {
|
|||||||
class Ab extends BaseAb {
|
class Ab extends BaseAb {
|
||||||
AbProfile profile;
|
AbProfile profile;
|
||||||
late final bool personal;
|
late final bool personal;
|
||||||
final sortTags = shouldSortTags().obs;
|
|
||||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
|
||||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||||
|
|
||||||
Ab(this.profile, this.personal);
|
Ab(this.profile, this.personal);
|
||||||
|
|||||||
@@ -535,6 +535,8 @@ class ChatModel with ChangeNotifier {
|
|||||||
void onVoiceCallClosed(String reason) {
|
void onVoiceCallClosed(String reason) {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
|
// We can always invoke "on_voice_call_closed"
|
||||||
|
// no matter if the `_voiceCallStatus` was `VoiceCallStatus.notStarted` or not.
|
||||||
parent.target?.invokeMethod("on_voice_call_closed");
|
parent.target?.invokeMethod("on_voice_call_closed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class CmFileModel {
|
|||||||
_onFileRemove(evt['remove']);
|
_onFileRemove(evt['remove']);
|
||||||
} else if (evt['create_dir'] != null) {
|
} else if (evt['create_dir'] != null) {
|
||||||
_onDirCreate(evt['create_dir']);
|
_onDirCreate(evt['create_dir']);
|
||||||
|
} else if (evt['rename'] != null) {
|
||||||
|
_onRename(evt['rename']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +61,6 @@ class CmFileModel {
|
|||||||
|
|
||||||
_dealOneJob(dynamic l, bool calcSpeed) {
|
_dealOneJob(dynamic l, bool calcSpeed) {
|
||||||
final data = TransferJobSerdeData.fromJson(l);
|
final data = TransferJobSerdeData.fromJson(l);
|
||||||
Client? client =
|
|
||||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
|
||||||
var jobTable = _jobTables[data.connId];
|
var jobTable = _jobTables[data.connId];
|
||||||
if (jobTable == null) {
|
if (jobTable == null) {
|
||||||
debugPrint("jobTable should not be null");
|
debugPrint("jobTable should not be null");
|
||||||
@@ -70,12 +70,7 @@ class CmFileModel {
|
|||||||
if (job == null) {
|
if (job == null) {
|
||||||
job = CmFileLog();
|
job = CmFileLog();
|
||||||
jobTable.add(job);
|
jobTable.add(job);
|
||||||
final currentSelectedTab =
|
_addUnread(data.connId);
|
||||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
|
||||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
|
||||||
currentSelectedTab.key == data.connId.toString())) {
|
|
||||||
client?.unreadChatMessageCount.value += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
job.id = data.id;
|
job.id = data.id;
|
||||||
job.action =
|
job.action =
|
||||||
@@ -167,8 +162,6 @@ class CmFileModel {
|
|||||||
try {
|
try {
|
||||||
dynamic d = jsonDecode(log);
|
dynamic d = jsonDecode(log);
|
||||||
FileActionLog data = FileActionLog.fromJson(d);
|
FileActionLog data = FileActionLog.fromJson(d);
|
||||||
Client? client =
|
|
||||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
|
||||||
var jobTable = _jobTables[data.connId];
|
var jobTable = _jobTables[data.connId];
|
||||||
if (jobTable == null) {
|
if (jobTable == null) {
|
||||||
debugPrint("jobTable should not be null");
|
debugPrint("jobTable should not be null");
|
||||||
@@ -179,17 +172,45 @@ class CmFileModel {
|
|||||||
..fileName = data.path
|
..fileName = data.path
|
||||||
..action = CmFileAction.createDir
|
..action = CmFileAction.createDir
|
||||||
..state = JobState.done);
|
..state = JobState.done);
|
||||||
final currentSelectedTab =
|
_addUnread(data.connId);
|
||||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
|
||||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
|
||||||
currentSelectedTab.key == data.connId.toString())) {
|
|
||||||
client?.unreadChatMessageCount.value += 1;
|
|
||||||
}
|
|
||||||
jobTable.refresh();
|
jobTable.refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('$e');
|
debugPrint('$e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onRename(dynamic log) {
|
||||||
|
try {
|
||||||
|
dynamic d = jsonDecode(log);
|
||||||
|
FileRenamenLog data = FileRenamenLog.fromJson(d);
|
||||||
|
var jobTable = _jobTables[data.connId];
|
||||||
|
if (jobTable == null) {
|
||||||
|
debugPrint("jobTable should not be null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final fileName = '${data.path} -> ${data.newName}';
|
||||||
|
jobTable.add(CmFileLog()
|
||||||
|
..id = 0
|
||||||
|
..fileName = fileName
|
||||||
|
..action = CmFileAction.rename
|
||||||
|
..state = JobState.done);
|
||||||
|
_addUnread(data.connId);
|
||||||
|
jobTable.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('$e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_addUnread(int connId) {
|
||||||
|
Client? client =
|
||||||
|
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId);
|
||||||
|
final currentSelectedTab =
|
||||||
|
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||||
|
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||||
|
currentSelectedTab.key == connId.toString())) {
|
||||||
|
client?.unreadChatMessageCount.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CmFileAction {
|
enum CmFileAction {
|
||||||
@@ -198,6 +219,7 @@ enum CmFileAction {
|
|||||||
localToRemote,
|
localToRemote,
|
||||||
remove,
|
remove,
|
||||||
createDir,
|
createDir,
|
||||||
|
rename,
|
||||||
}
|
}
|
||||||
|
|
||||||
class CmFileLog {
|
class CmFileLog {
|
||||||
@@ -285,3 +307,22 @@ class FileActionLog {
|
|||||||
dir: d['dir'] ?? false,
|
dir: d['dir'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FileRenamenLog {
|
||||||
|
int connId = 0;
|
||||||
|
String path = '';
|
||||||
|
String newName = '';
|
||||||
|
|
||||||
|
FileRenamenLog({
|
||||||
|
required this.connId,
|
||||||
|
required this.path,
|
||||||
|
required this.newName,
|
||||||
|
});
|
||||||
|
|
||||||
|
FileRenamenLog.fromJson(dynamic d)
|
||||||
|
: this(
|
||||||
|
connId: d['connId'] ?? 0,
|
||||||
|
path: d['path'] ?? '',
|
||||||
|
newName: d['newName'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/utils/event_loop.dart';
|
import 'package:flutter_hbb/utils/event_loop.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
@@ -642,6 +643,77 @@ class FileController {
|
|||||||
path: path,
|
path: path,
|
||||||
isRemote: !isLocal);
|
isRemote: !isLocal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> renameAction(Entry item, bool isLocal) async {
|
||||||
|
final textEditingController = TextEditingController(text: item.name);
|
||||||
|
String? errorText;
|
||||||
|
dialogManager?.show((setState, close, context) {
|
||||||
|
textEditingController.addListener(() {
|
||||||
|
if (errorText != null) {
|
||||||
|
setState(() {
|
||||||
|
errorText = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
submit() async {
|
||||||
|
final newName = textEditingController.text;
|
||||||
|
if (newName.isEmpty || newName == item.name) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (directory.value.entries.any((e) => e.name == newName)) {
|
||||||
|
setState(() {
|
||||||
|
errorText = translate("Already exists");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PathUtil.validName(newName, options.value.isWindows)) {
|
||||||
|
setState(() {
|
||||||
|
if (item.isDirectory) {
|
||||||
|
errorText = translate("Invalid folder name");
|
||||||
|
} else {
|
||||||
|
errorText = translate("Invalid file name");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await bind.sessionRenameFile(
|
||||||
|
sessionId: sessionId,
|
||||||
|
actId: JobController.jobID.next(),
|
||||||
|
path: item.path,
|
||||||
|
newName: newName,
|
||||||
|
isRemote: !isLocal);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
DialogTextField(
|
||||||
|
title: '${translate('Rename')} ${item.name}',
|
||||||
|
controller: textEditingController,
|
||||||
|
errorText: errorText,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
dialogButton(
|
||||||
|
"Cancel",
|
||||||
|
icon: Icon(Icons.close_rounded),
|
||||||
|
onPressed: close,
|
||||||
|
isOutline: true,
|
||||||
|
),
|
||||||
|
dialogButton(
|
||||||
|
"OK",
|
||||||
|
icon: Icon(Icons.done_rounded),
|
||||||
|
onPressed: submit,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JobController {
|
class JobController {
|
||||||
@@ -1083,6 +1155,13 @@ class PathUtil {
|
|||||||
final pathUtil = isWindows ? windowsContext : posixContext;
|
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||||
return pathUtil.dirname(path);
|
return pathUtil.dirname(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool validName(String name, bool isWindows) {
|
||||||
|
final unixFileNamePattern = RegExp(r'^[^/\0]+$');
|
||||||
|
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
|
||||||
|
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
|
||||||
|
return reg.hasMatch(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DirectoryOptions {
|
class DirectoryOptions {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class GroupModel {
|
|||||||
Future<void> pull({force = true, quiet = false}) async {
|
Future<void> pull({force = true, quiet = false}) async {
|
||||||
if (bind.isDisableGroupPanel()) return;
|
if (bind.isDisableGroupPanel()) return;
|
||||||
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
||||||
|
if (gFFI.userModel.networkError.isNotEmpty) return;
|
||||||
if (!force && initialized) return;
|
if (!force && initialized) return;
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
groupLoading.value = true;
|
groupLoading.value = true;
|
||||||
|
|||||||
@@ -1152,4 +1152,27 @@ class InputModel {
|
|||||||
platformFFI.stopDesktopWebListener();
|
platformFFI.stopDesktopWebListener();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onMobileBack() => tap(MouseButtons.right);
|
||||||
|
void onMobileHome() => tap(MouseButtons.wheel);
|
||||||
|
Future<void> onMobileApps() async {
|
||||||
|
sendMouse('down', MouseButtons.wheel);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
sendMouse('up', MouseButtons.wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate a key press event.
|
||||||
|
// `usbHidUsage` is the USB HID usage code of the key.
|
||||||
|
Future<void> tapHidKey(int usbHidUsage) async {
|
||||||
|
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true);
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onMobileVolumeUp() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage);
|
||||||
|
Future<void> onMobileVolumeDown() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage);
|
||||||
|
Future<void> onMobilePower() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,10 +192,10 @@ class FfiModel with ChangeNotifier {
|
|||||||
_permissions[k] = v == 'true';
|
_permissions[k] = v == 'true';
|
||||||
});
|
});
|
||||||
// Only inited at remote page
|
// Only inited at remote page
|
||||||
if (desktopType == DesktopType.remote) {
|
if (parent.target?.connType == ConnType.defaultConn) {
|
||||||
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||||
}
|
}
|
||||||
debugPrint('$_permissions');
|
debugPrint('updatePermission: $_permissions');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'use_texture_render') {
|
} else if (name == 'use_texture_render') {
|
||||||
_handleUseTextureRender(evt, sessionId, peerId);
|
_handleUseTextureRender(evt, sessionId, peerId);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Unknown event name: $name');
|
debugPrint('Event is not handled in the fixed branch: $name');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -438,20 +438,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
|
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
|
||||||
final running = evt['running'] == 'true';
|
final running = evt['running'] == 'true';
|
||||||
parent.target?.elevationModel.onPortableServiceRunning(running);
|
parent.target?.elevationModel.onPortableServiceRunning(running);
|
||||||
if (running) {
|
|
||||||
if (pi.primaryDisplay != kInvalidDisplayIndex) {
|
|
||||||
if (pi.currentDisplay != pi.primaryDisplay) {
|
|
||||||
// Notify to switch display
|
|
||||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
|
||||||
'elevated_switch_display_msg', '', parent.target!.dialogManager);
|
|
||||||
bind.sessionSwitchDisplay(
|
|
||||||
isDesktop: isDesktop,
|
|
||||||
sessionId: sessionId,
|
|
||||||
value: Int32List.fromList([pi.primaryDisplay]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAliasChanged(Map<String, dynamic> evt) {
|
handleAliasChanged(Map<String, dynamic> evt) {
|
||||||
@@ -737,6 +723,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
/// Handle the peer info event based on [evt].
|
/// Handle the peer info event based on [evt].
|
||||||
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
||||||
|
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
|
|
||||||
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
||||||
parent.target?.inputModel.updateKeyboardMode();
|
parent.target?.inputModel.updateKeyboardMode();
|
||||||
|
|
||||||
@@ -917,10 +905,12 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (parent.target?.connType == ConnType.defaultConn &&
|
if (parent.target?.connType == ConnType.defaultConn &&
|
||||||
parent.target != null &&
|
parent.target != null &&
|
||||||
parent.target!.ffiModel.permissions['keyboard'] != false) {
|
parent.target!.ffiModel.permissions['keyboard'] != false) {
|
||||||
Timer(
|
Timer(Duration(milliseconds: delayMSecs), () {
|
||||||
Duration(milliseconds: delayMSecs),
|
if (parent.target!.dialogManager.mobileActionsOverlayVisible.isTrue) {
|
||||||
() => parent.target!.dialogManager
|
parent.target!.dialogManager
|
||||||
.showMobileActionsOverlay(ffi: parent.target!));
|
.showMobileActionsOverlay(ffi: parent.target!);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -973,7 +963,9 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateLastCursorId(Map<String, dynamic> evt) {
|
updateLastCursorId(Map<String, dynamic> evt) {
|
||||||
parent.target?.cursorModel.id = int.parse(evt['id']);
|
// int.parse(evt['id']) may cause FormatException
|
||||||
|
// Unhandled Exception: FormatException: Positive input exceeds the limit of integer 18446744071749110741
|
||||||
|
parent.target?.cursorModel.id = evt['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCursorId(Map<String, dynamic> evt) {
|
handleCursorId(Map<String, dynamic> evt) {
|
||||||
@@ -1011,14 +1003,15 @@ class FfiModel with ChangeNotifier {
|
|||||||
// Notify to switch display
|
// Notify to switch display
|
||||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
||||||
'display_is_plugged_out_msg', '', parent.target!.dialogManager);
|
'display_is_plugged_out_msg', '', parent.target!.dialogManager);
|
||||||
final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex
|
final isPeerPrimaryDisplayValid =
|
||||||
? 0
|
pi.primaryDisplay == kInvalidDisplayIndex ||
|
||||||
: pi.primaryDisplay;
|
pi.primaryDisplay >= pi.displays.length;
|
||||||
final displays = newDisplay;
|
final newDisplay =
|
||||||
|
isPeerPrimaryDisplayValid ? 0 : pi.primaryDisplay;
|
||||||
bind.sessionSwitchDisplay(
|
bind.sessionSwitchDisplay(
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
value: Int32List.fromList([displays]),
|
value: Int32List.fromList([newDisplay]),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_pi.isSupportMultiUiSession) {
|
if (_pi.isSupportMultiUiSession) {
|
||||||
@@ -1183,25 +1176,28 @@ class ImageModel with ChangeNotifier {
|
|||||||
|
|
||||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||||
|
|
||||||
onRgba(int display, Uint8List rgba) {
|
clearImage() => _image = null;
|
||||||
final pid = parent.target?.id;
|
|
||||||
img.decodeImageFromPixels(
|
onRgba(int display, Uint8List rgba) async {
|
||||||
rgba,
|
|
||||||
parent.target?.ffiModel.rect?.width.toInt() ?? 0,
|
|
||||||
parent.target?.ffiModel.rect?.height.toInt() ?? 0,
|
|
||||||
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
|
||||||
onPixelsCopied: () {
|
|
||||||
// Unlock the rgba memory from rust codes.
|
|
||||||
platformFFI.nextRgba(sessionId, display);
|
|
||||||
}).then((image) {
|
|
||||||
if (parent.target?.id != pid) return;
|
|
||||||
try {
|
try {
|
||||||
// my throw exception, because the listener maybe already dispose
|
await decodeAndUpdate(display, rgba);
|
||||||
update(image);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('update image: $e');
|
debugPrint('onRgba error: $e');
|
||||||
}
|
}
|
||||||
});
|
platformFFI.nextRgba(sessionId, display);
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeAndUpdate(int display, Uint8List rgba) async {
|
||||||
|
final pid = parent.target?.id;
|
||||||
|
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
|
||||||
|
final image = await img.decodeImageFromPixels(
|
||||||
|
rgba,
|
||||||
|
rect?.width.toInt() ?? 0,
|
||||||
|
rect?.height.toInt() ?? 0,
|
||||||
|
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
||||||
|
);
|
||||||
|
if (parent.target?.id != pid) return;
|
||||||
|
await update(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(ui.Image? image) async {
|
update(ui.Image? image) async {
|
||||||
@@ -1573,22 +1569,24 @@ class CanvasModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScale(double v) {
|
updateScale(double v, Offset focalPoint) {
|
||||||
if (parent.target?.imageModel.image == null) return;
|
if (parent.target?.imageModel.image == null) return;
|
||||||
final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0);
|
final s = _scale;
|
||||||
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
|
||||||
final px0 = (offset.dx - r.left) * _scale;
|
|
||||||
final py0 = (offset.dy - r.top) * _scale;
|
|
||||||
_scale *= v;
|
_scale *= v;
|
||||||
final maxs = parent.target?.imageModel.maxScale ?? 1;
|
final maxs = parent.target?.imageModel.maxScale ?? 1;
|
||||||
final mins = parent.target?.imageModel.minScale ?? 1;
|
final mins = parent.target?.imageModel.minScale ?? 1;
|
||||||
if (_scale > maxs) _scale = maxs;
|
if (_scale > maxs) _scale = maxs;
|
||||||
if (_scale < mins) _scale = mins;
|
if (_scale < mins) _scale = mins;
|
||||||
r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
// (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX
|
||||||
final px1 = (offset.dx - r.left) * _scale;
|
// _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2
|
||||||
final py1 = (offset.dy - r.top) * _scale;
|
_x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale;
|
||||||
_x -= px1 - px0;
|
final adjustForKeyboard =
|
||||||
_y -= py1 - py0;
|
parent.target?.cursorModel.adjustForKeyboard() ?? 0.0;
|
||||||
|
// (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY
|
||||||
|
// _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2
|
||||||
|
_y = focalPoint.dy +
|
||||||
|
adjustForKeyboard -
|
||||||
|
(focalPoint.dy - _y + adjustForKeyboard) / s * _scale;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1619,7 +1617,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
// data for cursor
|
// data for cursor
|
||||||
class CursorData {
|
class CursorData {
|
||||||
final String peerId;
|
final String peerId;
|
||||||
final int id;
|
final String id;
|
||||||
final img2.Image image;
|
final img2.Image image;
|
||||||
double scale;
|
double scale;
|
||||||
Uint8List? data;
|
Uint8List? data;
|
||||||
@@ -1699,12 +1697,12 @@ const _forbiddenCursorPng =
|
|||||||
const _defaultCursorPng =
|
const _defaultCursorPng =
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
||||||
|
|
||||||
const kPreForbiddenCursorId = -2;
|
const kPreForbiddenCursorId = "-2";
|
||||||
final preForbiddenCursor = PredefinedCursor(
|
final preForbiddenCursor = PredefinedCursor(
|
||||||
png: _forbiddenCursorPng,
|
png: _forbiddenCursorPng,
|
||||||
id: kPreForbiddenCursorId,
|
id: kPreForbiddenCursorId,
|
||||||
);
|
);
|
||||||
const kPreDefaultCursorId = -1;
|
const kPreDefaultCursorId = "-1";
|
||||||
final preDefaultCursor = PredefinedCursor(
|
final preDefaultCursor = PredefinedCursor(
|
||||||
png: _defaultCursorPng,
|
png: _defaultCursorPng,
|
||||||
id: kPreDefaultCursorId,
|
id: kPreDefaultCursorId,
|
||||||
@@ -1717,7 +1715,7 @@ class PredefinedCursor {
|
|||||||
img2.Image? _image2;
|
img2.Image? _image2;
|
||||||
CursorData? _cache;
|
CursorData? _cache;
|
||||||
String png;
|
String png;
|
||||||
int id;
|
String id;
|
||||||
double Function(double)? hotxGetter;
|
double Function(double)? hotxGetter;
|
||||||
double Function(double)? hotyGetter;
|
double Function(double)? hotyGetter;
|
||||||
|
|
||||||
@@ -1733,7 +1731,7 @@ class PredefinedCursor {
|
|||||||
_image2 = img2.decodePng(base64Decode(png));
|
_image2 = img2.decodePng(base64Decode(png));
|
||||||
if (_image2 != null) {
|
if (_image2 != null) {
|
||||||
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
||||||
if (isWindows && id == kPreForbiddenCursorId) {
|
if (id == kPreForbiddenCursorId) {
|
||||||
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
|
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1775,13 +1773,15 @@ class PredefinedCursor {
|
|||||||
|
|
||||||
class CursorModel with ChangeNotifier {
|
class CursorModel with ChangeNotifier {
|
||||||
ui.Image? _image;
|
ui.Image? _image;
|
||||||
final _images = <int, Tuple3<ui.Image, double, double>>{};
|
final _images = <String, Tuple3<ui.Image, double, double>>{};
|
||||||
CursorData? _cache;
|
CursorData? _cache;
|
||||||
final _cacheMap = <int, CursorData>{};
|
final _cacheMap = <String, CursorData>{};
|
||||||
final _cacheKeys = <String>{};
|
final _cacheKeys = <String>{};
|
||||||
double _x = -10000;
|
double _x = -10000;
|
||||||
double _y = -10000;
|
double _y = -10000;
|
||||||
int _id = -1;
|
// int.parse(evt['id']) may cause FormatException
|
||||||
|
// So we use String here.
|
||||||
|
String _id = "-1";
|
||||||
double _hotx = 0;
|
double _hotx = 0;
|
||||||
double _hoty = 0;
|
double _hoty = 0;
|
||||||
double _displayOriginX = 0;
|
double _displayOriginX = 0;
|
||||||
@@ -1795,6 +1795,33 @@ class CursorModel with ChangeNotifier {
|
|||||||
String peerId = '';
|
String peerId = '';
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
|
// Only for mobile, touch mode
|
||||||
|
// To block touch event above the KeyHelpTools
|
||||||
|
//
|
||||||
|
// A better way is to not listen events from the KeyHelpTools.
|
||||||
|
// But we're now using a Container(child: Stack(...)) to wrap the KeyHelpTools,
|
||||||
|
// and the listener is on the Container.
|
||||||
|
Rect? _keyHelpToolsRect;
|
||||||
|
// `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap()
|
||||||
|
// Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position.
|
||||||
|
bool _lastIsBlocked = false;
|
||||||
|
double _yForKeyboardAdjust = 0;
|
||||||
|
|
||||||
|
keyHelpToolsVisibilityChanged(Rect? r) {
|
||||||
|
_keyHelpToolsRect = r;
|
||||||
|
if (r == null) {
|
||||||
|
_lastIsBlocked = false;
|
||||||
|
} else {
|
||||||
|
// Block the touch event is safe here.
|
||||||
|
// `lastIsBlocked` is only used in onDoubleTap() to block the touch event from the KeyHelpTools.
|
||||||
|
// `lastIsBlocked` will be set when the cursor is moving or touch somewhere else.
|
||||||
|
_lastIsBlocked = true;
|
||||||
|
}
|
||||||
|
_yForKeyboardAdjust = _y;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastIsBlocked => _lastIsBlocked;
|
||||||
|
|
||||||
ui.Image? get image => _image;
|
ui.Image? get image => _image;
|
||||||
CursorData? get cache => _cache;
|
CursorData? get cache => _cache;
|
||||||
|
|
||||||
@@ -1808,7 +1835,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
double get hotx => _hotx;
|
double get hotx => _hotx;
|
||||||
double get hoty => _hoty;
|
double get hoty => _hoty;
|
||||||
|
|
||||||
set id(int id) => _id = id;
|
set id(String id) => _id = id;
|
||||||
|
|
||||||
bool get isPeerControlProtected =>
|
bool get isPeerControlProtected =>
|
||||||
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
|
DateTime.now().difference(_lastPeerMouse).inMilliseconds <
|
||||||
@@ -1839,28 +1866,52 @@ class CursorModel with ChangeNotifier {
|
|||||||
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
|
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom;
|
||||||
|
get scale => parent.target?.canvasModel.scale ?? 1.0;
|
||||||
|
|
||||||
double adjustForKeyboard() {
|
double adjustForKeyboard() {
|
||||||
|
if (keyboardHeight < 100) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
final m = MediaQueryData.fromWindow(ui.window);
|
final m = MediaQueryData.fromWindow(ui.window);
|
||||||
var keyboardHeight = m.viewInsets.bottom;
|
|
||||||
final size = m.size;
|
final size = m.size;
|
||||||
if (keyboardHeight < 100) return 0;
|
|
||||||
final s = parent.target?.canvasModel.scale ?? 1.0;
|
|
||||||
final thresh = (size.height - keyboardHeight) / 2;
|
final thresh = (size.height - keyboardHeight) / 2;
|
||||||
var h = (_y - getVisibleRect().top) * s; // local physical display height
|
final h = (_yForKeyboardAdjust - getVisibleRect().top) *
|
||||||
|
scale; // local physical display height
|
||||||
return h - thresh;
|
return h - thresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
move(double x, double y) {
|
// mobile Soft keyboard, block touch event from the KeyHelpTools
|
||||||
moveLocal(x, y);
|
shouldBlock(double x, double y) {
|
||||||
parent.target?.inputModel.moveMouse(_x, _y);
|
if (!(parent.target?.ffiModel.touchMode ?? false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_keyHelpToolsRect == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLocal(double x, double y) {
|
move(double x, double y) {
|
||||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
if (shouldBlock(x, y)) {
|
||||||
|
_lastIsBlocked = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_lastIsBlocked = false;
|
||||||
|
moveLocal(x, y, adjust: adjustForKeyboard());
|
||||||
|
parent.target?.inputModel.moveMouse(_x, _y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLocal(double x, double y, {double adjust = 0}) {
|
||||||
final xoffset = parent.target?.canvasModel.x ?? 0;
|
final xoffset = parent.target?.canvasModel.x ?? 0;
|
||||||
final yoffset = parent.target?.canvasModel.y ?? 0;
|
final yoffset = parent.target?.canvasModel.y ?? 0;
|
||||||
_x = (x - xoffset) / scale + _displayOriginX;
|
_x = (x - xoffset) / scale + _displayOriginX;
|
||||||
_y = (y - yoffset) / scale + _displayOriginY;
|
_y = (y - yoffset + adjust) / scale + _displayOriginY;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1986,7 +2037,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCursorData(Map<String, dynamic> evt) async {
|
updateCursorData(Map<String, dynamic> evt) async {
|
||||||
final id = int.parse(evt['id']);
|
final id = evt['id'];
|
||||||
final hotx = double.parse(evt['hotx']);
|
final hotx = double.parse(evt['hotx']);
|
||||||
final hoty = double.parse(evt['hoty']);
|
final hoty = double.parse(evt['hoty']);
|
||||||
final width = int.parse(evt['width']);
|
final width = int.parse(evt['width']);
|
||||||
@@ -2011,7 +2062,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
Future<bool> _updateCache(
|
Future<bool> _updateCache(
|
||||||
Uint8List rgba,
|
Uint8List rgba,
|
||||||
ui.Image image,
|
ui.Image image,
|
||||||
int id,
|
String id,
|
||||||
double hotx,
|
double hotx,
|
||||||
double hoty,
|
double hoty,
|
||||||
int w,
|
int w,
|
||||||
@@ -2393,9 +2444,10 @@ class FFI {
|
|||||||
cursorModel.peerId = id;
|
cursorModel.peerId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isNewPeer = tabWindowId == null;
|
||||||
// If tabWindowId != null, this session is a "tab -> window" one.
|
// If tabWindowId != null, this session is a "tab -> window" one.
|
||||||
// Else this session is a new one.
|
// Else this session is a new one.
|
||||||
if (tabWindowId == null) {
|
if (isNewPeer) {
|
||||||
// ignore: unused_local_variable
|
// ignore: unused_local_variable
|
||||||
final addRes = bind.sessionAddSync(
|
final addRes = bind.sessionAddSync(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
@@ -2414,20 +2466,32 @@ class FFI {
|
|||||||
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
|
'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final addRes = bind.sessionAddExistedSync(id: id, sessionId: sessionId);
|
final addRes = bind.sessionAddExistedSync(
|
||||||
|
id: id, sessionId: sessionId, displays: Int32List.fromList(displays));
|
||||||
if (addRes != '') {
|
if (addRes != '') {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'Unreachable, failed to add existed session to $id, $addRes');
|
'Unreachable, failed to add existed session to $id, $addRes');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bind.sessionTryAddDisplay(
|
|
||||||
sessionId: sessionId, displays: Int32List.fromList(displays));
|
|
||||||
ffiModel.pi.currentDisplay = display;
|
ffiModel.pi.currentDisplay = display;
|
||||||
}
|
}
|
||||||
if (isDesktop && connType == ConnType.defaultConn) {
|
if (isDesktop && connType == ConnType.defaultConn) {
|
||||||
textureModel.updateCurrentDisplay(display ?? 0);
|
textureModel.updateCurrentDisplay(display ?? 0);
|
||||||
}
|
}
|
||||||
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
|
||||||
|
// CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions.
|
||||||
|
// Though the stream is returned immediately, the stream may not be ready.
|
||||||
|
// Any operations that depend on the stream should be carefully handled.
|
||||||
|
late final Stream<EventToUI> stream;
|
||||||
|
if (isNewPeer || display == null || displays == null) {
|
||||||
|
stream = bind.sessionStart(sessionId: sessionId, id: id);
|
||||||
|
} else {
|
||||||
|
// We have to put displays in `sessionStart()` to make sure the stream is ready
|
||||||
|
// and then the displays' capturing requests can be sent.
|
||||||
|
stream = bind.sessionStartWithDisplays(
|
||||||
|
sessionId: sessionId, id: id, displays: Int32List.fromList(displays));
|
||||||
|
}
|
||||||
|
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
platformFFI.setRgbaCallback((int display, Uint8List data) {
|
platformFFI.setRgbaCallback((int display, Uint8List data) {
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
@@ -2438,14 +2502,6 @@ class FFI {
|
|||||||
|
|
||||||
final cb = ffiModel.startEventListener(sessionId, id);
|
final cb = ffiModel.startEventListener(sessionId, id);
|
||||||
|
|
||||||
// Force refresh displays.
|
|
||||||
// The controlled side may not refresh the image when the (peer,display) is already subscribed.
|
|
||||||
if (displays != null) {
|
|
||||||
for (final display in displays) {
|
|
||||||
bind.sessionRefresh(sessionId: sessionId, display: display);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imageModel.updateUserTextureRender();
|
imageModel.updateUserTextureRender();
|
||||||
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
||||||
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
||||||
@@ -2496,29 +2552,30 @@ class FFI {
|
|||||||
}
|
}
|
||||||
} else if (message is EventToUI_Rgba) {
|
} else if (message is EventToUI_Rgba) {
|
||||||
final display = message.field0;
|
final display = message.field0;
|
||||||
if (imageModel.useTextureRender) {
|
|
||||||
debugPrint("EventToUI_Rgba display:$display");
|
|
||||||
textureModel.setTextureType(display: display, gpuTexture: false);
|
|
||||||
onEvent2UIRgba();
|
|
||||||
} else {
|
|
||||||
// Fetch the image buffer from rust codes.
|
// Fetch the image buffer from rust codes.
|
||||||
final sz = platformFFI.getRgbaSize(sessionId, display);
|
final sz = platformFFI.getRgbaSize(sessionId, display);
|
||||||
if (sz == 0) {
|
if (sz == 0) {
|
||||||
|
platformFFI.nextRgba(sessionId, display);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
||||||
if (rgba != null) {
|
if (rgba != null) {
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
imageModel.onRgba(display, rgba);
|
await imageModel.onRgba(display, rgba);
|
||||||
}
|
} else {
|
||||||
|
platformFFI.nextRgba(sessionId, display);
|
||||||
}
|
}
|
||||||
} else if (message is EventToUI_Texture) {
|
} else if (message is EventToUI_Texture) {
|
||||||
final display = message.field0;
|
final display = message.field0;
|
||||||
debugPrint("EventToUI_Texture display:$display");
|
final gpuTexture = message.field1;
|
||||||
if (hasGpuTextureRender) {
|
debugPrint(
|
||||||
textureModel.setTextureType(display: display, gpuTexture: true);
|
"EventToUI_Texture display:$display, gpuTexture:$gpuTexture");
|
||||||
onEvent2UIRgba();
|
if (gpuTexture && !hasGpuTextureRender) {
|
||||||
|
debugPrint('the gpuTexture is not supported.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
textureModel.setTextureType(display: display, gpuTexture: gpuTexture);
|
||||||
|
onEvent2UIRgba();
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
});
|
});
|
||||||
@@ -2554,8 +2611,9 @@ class FFI {
|
|||||||
remember: remember);
|
remember: remember);
|
||||||
}
|
}
|
||||||
|
|
||||||
void send2FA(SessionID sessionId, String code) {
|
void send2FA(SessionID sessionId, String code, bool trustThisDevice) {
|
||||||
bind.sessionSend2Fa(sessionId: sessionId, code: code);
|
bind.sessionSend2Fa(
|
||||||
|
sessionId: sessionId, code: code, trustThisDevice: trustThisDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close the remote session.
|
/// Close the remote session.
|
||||||
@@ -2572,7 +2630,7 @@ class FFI {
|
|||||||
canvasModel.scale,
|
canvasModel.scale,
|
||||||
ffiModel.pi.currentDisplay);
|
ffiModel.pi.currentDisplay);
|
||||||
}
|
}
|
||||||
imageModel.update(null);
|
await imageModel.update(null);
|
||||||
cursorModel.clear();
|
cursorModel.clear();
|
||||||
ffiModel.clear();
|
ffiModel.clear();
|
||||||
canvasModel.clear();
|
canvasModel.clear();
|
||||||
@@ -2685,6 +2743,7 @@ class PeerInfo with ChangeNotifier {
|
|||||||
|
|
||||||
bool get isSupportMultiDisplay =>
|
bool get isSupportMultiDisplay =>
|
||||||
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
||||||
|
bool get forceTextureRender => currentDisplay == kAllDisplayValue;
|
||||||
|
|
||||||
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
||||||
|
|
||||||
@@ -2693,30 +2752,32 @@ class PeerInfo with ChangeNotifier {
|
|||||||
bool get isAmyuniIdd =>
|
bool get isAmyuniIdd =>
|
||||||
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
||||||
|
|
||||||
Display? tryGetDisplay() {
|
Display? tryGetDisplay({int? display}) {
|
||||||
if (displays.isEmpty) {
|
if (displays.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay == kAllDisplayValue) {
|
display ??= currentDisplay;
|
||||||
|
if (display == kAllDisplayValue) {
|
||||||
return displays[0];
|
return displays[0];
|
||||||
} else {
|
} else {
|
||||||
if (currentDisplay > 0 && currentDisplay < displays.length) {
|
if (display > 0 && display < displays.length) {
|
||||||
return displays[currentDisplay];
|
return displays[display];
|
||||||
} else {
|
} else {
|
||||||
return displays[0];
|
return displays[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Display? tryGetDisplayIfNotAllDisplay() {
|
Display? tryGetDisplayIfNotAllDisplay({int? display}) {
|
||||||
if (displays.isEmpty) {
|
if (displays.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay == kAllDisplayValue) {
|
display ??= currentDisplay;
|
||||||
|
if (display == kAllDisplayValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (currentDisplay >= 0 && currentDisplay < displays.length) {
|
if (display >= 0 && display < displays.length) {
|
||||||
return displays[currentDisplay];
|
return displays[display];
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2740,6 +2801,12 @@ class PeerInfo with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect? getDisplayRect(int display) {
|
||||||
|
final d = tryGetDisplayIfNotAllDisplay(display: display);
|
||||||
|
if (d == null) return null;
|
||||||
|
return Rect.fromLTWH(d.x, d.y, d.width.toDouble(), d.height.toDouble());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasKey = 'canvas';
|
const canvasKey = 'canvas';
|
||||||
|
|||||||
@@ -117,9 +117,13 @@ class PlatformFFI {
|
|||||||
? DynamicLibrary.open('librustdesk.so')
|
? DynamicLibrary.open('librustdesk.so')
|
||||||
: isWindows
|
: isWindows
|
||||||
? DynamicLibrary.open('librustdesk.dll')
|
? DynamicLibrary.open('librustdesk.dll')
|
||||||
: isMacOS
|
:
|
||||||
? DynamicLibrary.open("liblibrustdesk.dylib")
|
// Use executable itself as the dynamic library for MacOS.
|
||||||
: DynamicLibrary.process();
|
// Multiple dylib instances will cause some global instances to be invalid.
|
||||||
|
// eg. `lazy_static` objects in rust side, will be created more than once, which is not expected.
|
||||||
|
//
|
||||||
|
// isMacOS? DynamicLibrary.open("liblibrustdesk.dylib") :
|
||||||
|
DynamicLibrary.process();
|
||||||
debugPrint('initializing FFI $_appType');
|
debugPrint('initializing FFI $_appType');
|
||||||
try {
|
try {
|
||||||
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
|
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
|
||||||
@@ -132,9 +136,10 @@ class PlatformFFI {
|
|||||||
_ffiBind = RustdeskImpl(dylib);
|
_ffiBind = RustdeskImpl(dylib);
|
||||||
|
|
||||||
if (isLinux) {
|
if (isLinux) {
|
||||||
// Start a dbus service, no need to await
|
if (isMain) {
|
||||||
|
// Start a dbus service for uri links, no need to await
|
||||||
_ffiBind.mainStartDbusServer();
|
_ffiBind.mainStartDbusServer();
|
||||||
_ffiBind.mainStartPa();
|
}
|
||||||
} else if (isMacOS && isMain) {
|
} else if (isMacOS && isMain) {
|
||||||
// Start ipc service for uri links.
|
// Start ipc service for uri links.
|
||||||
_ffiBind.mainStartIpcUrlServer();
|
_ffiBind.mainStartIpcUrlServer();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -176,6 +177,11 @@ class ServerModel with ChangeNotifier {
|
|||||||
await timerCallback();
|
await timerCallback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial keyboard status is off on mobile
|
||||||
|
if (isMobile) {
|
||||||
|
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 1. check android permission
|
/// 1. check android permission
|
||||||
@@ -190,7 +196,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
|
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
|
||||||
} else {
|
} else {
|
||||||
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
|
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
|
||||||
_audioOk = audioOption.isEmpty;
|
_audioOk = audioOption != 'N';
|
||||||
}
|
}
|
||||||
|
|
||||||
// file
|
// file
|
||||||
@@ -200,7 +206,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
final fileOption =
|
final fileOption =
|
||||||
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||||
_fileOk = fileOption.isEmpty;
|
_fileOk = fileOption != 'N';
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -226,8 +232,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
_approveMode = approveMode;
|
_approveMode = approveMode;
|
||||||
update = true;
|
update = true;
|
||||||
}
|
}
|
||||||
var stopped = option2bool(
|
var stopped = await mainGetBoolOption(kOptionStopService);
|
||||||
"stop-service", await bind.mainGetOption(key: "stop-service"));
|
|
||||||
final oldPwdText = _serverPasswd.text;
|
final oldPwdText = _serverPasswd.text;
|
||||||
if (stopped ||
|
if (stopped ||
|
||||||
verificationMethod == kUsePermanentPassword ||
|
verificationMethod == kUsePermanentPassword ||
|
||||||
@@ -340,6 +345,20 @@ class ServerModel with ChangeNotifier {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> checkFloatingWindowPermission() async {
|
||||||
|
debugPrint("androidVersion $androidVersion");
|
||||||
|
if (androidVersion < 23) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||||
|
debugPrint("alert window permission already granted");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var res = await AndroidPermissionManager.request(kSystemAlertWindow);
|
||||||
|
debugPrint("alert window permission request result: $res");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle the screen sharing service.
|
/// Toggle the screen sharing service.
|
||||||
toggleService() async {
|
toggleService() async {
|
||||||
if (_isStart) {
|
if (_isStart) {
|
||||||
@@ -367,6 +386,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await checkRequestNotificationPermission();
|
await checkRequestNotificationPermission();
|
||||||
|
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
|
||||||
|
await checkFloatingWindowPermission();
|
||||||
|
}
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
await AndroidPermissionManager.request(kManageExternalStorage);
|
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||||
}
|
}
|
||||||
@@ -405,7 +427,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
await bind.mainStartService();
|
await bind.mainStartService();
|
||||||
updateClientState();
|
updateClientState();
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
WakelockPlus.enable();
|
androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +520,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
if (_clients.length != oldClientLenght) {
|
if (_clients.length != oldClientLenght) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +555,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (isAndroid && !client.authorized) showLoginDialog(client);
|
if (isAndroid && !client.authorized) showLoginDialog(client);
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to call loginRequest,error:$e");
|
debugPrint("Failed to call loginRequest,error:$e");
|
||||||
}
|
}
|
||||||
@@ -652,6 +676,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
final index = _clients.indexOf(client);
|
final index = _clients.indexOf(client);
|
||||||
tabController.remove(index);
|
tabController.remove(index);
|
||||||
_clients.remove(client);
|
_clients.remove(client);
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,6 +700,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (desktopType == DesktopType.cm && _clients.isEmpty) {
|
if (desktopType == DesktopType.cm && _clients.isEmpty) {
|
||||||
hideCmWindow();
|
hideCmWindow();
|
||||||
}
|
}
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("onClientRemove failed,error:$e");
|
debugPrint("onClientRemove failed,error:$e");
|
||||||
@@ -686,6 +712,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
||||||
_clients.clear();
|
_clients.clear();
|
||||||
tabController.state.value.tabs.clear();
|
tabController.state.value.tabs.clear();
|
||||||
|
if (isAndroid) androidUpdatekeepScreenOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
void jumpTo(int id) {
|
void jumpTo(int id) {
|
||||||
@@ -723,6 +750,27 @@ class ServerModel with ChangeNotifier {
|
|||||||
debugPrint("updateVoiceCallState failed: $e");
|
debugPrint("updateVoiceCallState failed: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void androidUpdatekeepScreenOn() async {
|
||||||
|
if (!isAndroid) return;
|
||||||
|
var floatingWindowDisabled =
|
||||||
|
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
||||||
|
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
||||||
|
final keepScreenOn = floatingWindowDisabled
|
||||||
|
? KeepScreenOn.never
|
||||||
|
: optionToKeepScreenOn(
|
||||||
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
|
||||||
|
final on = ((keepScreenOn == KeepScreenOn.serviceOn) && _isStart) ||
|
||||||
|
(keepScreenOn == KeepScreenOn.duringControlled &&
|
||||||
|
_clients.map((e) => !e.disconnected).isNotEmpty);
|
||||||
|
if (on != await WakelockPlus.enabled) {
|
||||||
|
if (on) {
|
||||||
|
WakelockPlus.enable();
|
||||||
|
} else {
|
||||||
|
WakelockPlus.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ClientType {
|
enum ClientType {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class StateGlobal {
|
|||||||
bool _isMinimized = false;
|
bool _isMinimized = false;
|
||||||
final RxBool isMaximized = false.obs;
|
final RxBool isMaximized = false.obs;
|
||||||
final RxBool _showTabBar = true.obs;
|
final RxBool _showTabBar = true.obs;
|
||||||
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize);
|
final RxDouble _resizeEdgeSize = RxDouble(windowResizeEdgeSize);
|
||||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||||
final RxBool showRemoteToolBar = false.obs;
|
final RxBool showRemoteToolBar = false.obs;
|
||||||
final svcStatus = SvcStatus.notReady.obs;
|
final svcStatus = SvcStatus.notReady.obs;
|
||||||
@@ -93,7 +93,7 @@ class StateGlobal {
|
|||||||
? kFullScreenEdgeSize
|
? kFullScreenEdgeSize
|
||||||
: isMaximized.isTrue
|
: isMaximized.isTrue
|
||||||
? kMaximizeEdgeSize
|
? kMaximizeEdgeSize
|
||||||
: windowEdgeSize;
|
: windowResizeEdgeSize;
|
||||||
|
|
||||||
String getInputSource({bool force = false}) {
|
String getInputSource({bool force = false}) {
|
||||||
if (force || _inputSource.isEmpty) {
|
if (force || _inputSource.isEmpty) {
|
||||||
|
|||||||
@@ -17,13 +17,23 @@ bool refreshingUser = false;
|
|||||||
class UserModel {
|
class UserModel {
|
||||||
final RxString userName = ''.obs;
|
final RxString userName = ''.obs;
|
||||||
final RxBool isAdmin = false.obs;
|
final RxBool isAdmin = false.obs;
|
||||||
|
final RxString networkError = ''.obs;
|
||||||
bool get isLogin => userName.isNotEmpty;
|
bool get isLogin => userName.isNotEmpty;
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
UserModel(this.parent);
|
UserModel(this.parent) {
|
||||||
|
userName.listen((p0) {
|
||||||
|
// When user name becomes empty, show login button
|
||||||
|
// When user name becomes non-empty:
|
||||||
|
// For _updateLocalUserInfo, network error will be set later
|
||||||
|
// For login success, should clear network error
|
||||||
|
networkError.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void refreshCurrentUser() async {
|
void refreshCurrentUser() async {
|
||||||
if (bind.isDisableAccount()) return;
|
if (bind.isDisableAccount()) return;
|
||||||
|
networkError.value = '';
|
||||||
final token = bind.mainGetLocalOption(key: 'access_token');
|
final token = bind.mainGetLocalOption(key: 'access_token');
|
||||||
if (token == '') {
|
if (token == '') {
|
||||||
await updateOtherModels();
|
await updateOtherModels();
|
||||||
@@ -38,12 +48,18 @@ class UserModel {
|
|||||||
if (refreshingUser) return;
|
if (refreshingUser) return;
|
||||||
try {
|
try {
|
||||||
refreshingUser = true;
|
refreshingUser = true;
|
||||||
final response = await http.post(Uri.parse('$url/api/currentUser'),
|
final http.Response response;
|
||||||
|
try {
|
||||||
|
response = await http.post(Uri.parse('$url/api/currentUser'),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer $token'
|
'Authorization': 'Bearer $token'
|
||||||
},
|
},
|
||||||
body: json.encode(body));
|
body: json.encode(body));
|
||||||
|
} catch (e) {
|
||||||
|
networkError.value = e.toString();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
refreshingUser = false;
|
refreshingUser = false;
|
||||||
final status = response.statusCode;
|
final status = response.statusCode;
|
||||||
if (status == 401 || status == 400) {
|
if (status == 401 || status == 400) {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ Future<ui.Image?> decodeImageFromPixels(
|
|||||||
int? rowBytes,
|
int? rowBytes,
|
||||||
int? targetWidth,
|
int? targetWidth,
|
||||||
int? targetHeight,
|
int? targetHeight,
|
||||||
VoidCallback? onPixelsCopied,
|
|
||||||
bool allowUpscaling = true,
|
bool allowUpscaling = true,
|
||||||
}) async {
|
}) async {
|
||||||
if (targetWidth != null) {
|
if (targetWidth != null) {
|
||||||
@@ -34,7 +33,6 @@ Future<ui.Image?> decodeImageFromPixels(
|
|||||||
final ui.ImmutableBuffer buffer;
|
final ui.ImmutableBuffer buffer;
|
||||||
try {
|
try {
|
||||||
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||||
onPixelsCopied?.call();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,15 +59,13 @@ class RustdeskImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String sessionAddExistedSync(
|
String sessionAddExistedSync(
|
||||||
{required String id, required UuidValue sessionId, dynamic hint}) {
|
{required String id,
|
||||||
|
required UuidValue sessionId,
|
||||||
|
required Int32List displays,
|
||||||
|
dynamic hint}) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void sessionTryAddDisplay(
|
|
||||||
{required UuidValue sessionId,
|
|
||||||
required Int32List displays,
|
|
||||||
dynamic hint}) {}
|
|
||||||
|
|
||||||
String sessionAddSync(
|
String sessionAddSync(
|
||||||
{required UuidValue sessionId,
|
{required UuidValue sessionId,
|
||||||
required String id,
|
required String id,
|
||||||
@@ -94,6 +92,14 @@ class RustdeskImpl {
|
|||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<EventToUI> sessionStartWithDisplays(
|
||||||
|
{required UuidValue sessionId,
|
||||||
|
required String id,
|
||||||
|
required Int32List displays,
|
||||||
|
dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool?> sessionGetRemember(
|
Future<bool?> sessionGetRemember(
|
||||||
{required UuidValue sessionId, dynamic hint}) {
|
{required UuidValue sessionId, dynamic hint}) {
|
||||||
return Future(
|
return Future(
|
||||||
@@ -136,7 +142,10 @@ class RustdeskImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sessionSend2Fa(
|
Future<void> sessionSend2Fa(
|
||||||
{required UuidValue sessionId, required String code, dynamic hint}) {
|
{required UuidValue sessionId,
|
||||||
|
required String code,
|
||||||
|
required bool trustThisDevice,
|
||||||
|
dynamic hint}) {
|
||||||
return Future(() => js.context.callMethod('setByName', ['send_2fa', code]));
|
return Future(() => js.context.callMethod('setByName', ['send_2fa', code]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,7 +946,7 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> mainVideoSaveDirectory({required bool root, dynamic hint}) {
|
String mainVideoSaveDirectory({required bool root, dynamic hint}) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1406,11 +1415,7 @@ class RustdeskImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> mainStartPa({dynamic hint}) {
|
bool mainHideDock({dynamic hint}) {
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool mainHideDocker({dynamic hint}) {
|
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1612,5 +1617,38 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool mainHasValidBotSync({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> mainVerifyBot({required String token, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
String mainGetUnlockPin({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
String mainSetUnlockPin({required String pin, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sessionGetEnableTrustedDevices(
|
||||||
|
{required UuidValue sessionId, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> mainGetTrustedDevices({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> mainRemoveTrustedDevices({required String json, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> mainClearTrustedDevices({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,5 +45,7 @@
|
|||||||
<string>Record the sound from microphone for the purpose of the remote desktop.</string>
|
<string>Record the sound from microphone for the purpose of the remote desktop.</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<string>1</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
2
flutter/ndk_x86.sh
Executable file
2
flutter/ndk_x86.sh
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter
|
||||||
@@ -335,7 +335,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: 3535741662c5b7529e182227a277a8551aed3398
|
resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926"
|
||||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@@ -377,7 +377,7 @@ packages:
|
|||||||
path: "."
|
path: "."
|
||||||
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
|
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
|
||||||
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
|
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
|
||||||
url: "https://github.com/21pages/dynamic_layouts.git"
|
url: "https://github.com/rustdesk-org/dynamic_layouts.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1+1"
|
version: "0.0.1+1"
|
||||||
external_path:
|
external_path:
|
||||||
@@ -511,7 +511,7 @@ packages:
|
|||||||
path: "."
|
path: "."
|
||||||
ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
||||||
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
||||||
url: "https://github.com/21pages/flutter_gpu_texture_renderer"
|
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
flutter_improved_scrolling:
|
flutter_improved_scrolling:
|
||||||
@@ -790,13 +790,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+1"
|
version: "0.2.1+1"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.18.1"
|
version: "0.19.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.2.5+39
|
version: 1.3.0+46
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
@@ -79,7 +79,7 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
||||||
uni_links: ^0.5.1
|
uni_links: ^0.5.1
|
||||||
uni_links_desktop: ^0.1.7
|
uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works
|
||||||
path: ^1.8.1
|
path: ^1.8.1
|
||||||
auto_size_text: ^3.0.0
|
auto_size_text: ^3.0.0
|
||||||
bot_toast: ^4.0.3
|
bot_toast: ^4.0.3
|
||||||
@@ -92,14 +92,14 @@ dependencies:
|
|||||||
dropdown_button2: ^2.0.0
|
dropdown_button2: ^2.0.0
|
||||||
flutter_gpu_texture_renderer:
|
flutter_gpu_texture_renderer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/21pages/flutter_gpu_texture_renderer
|
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
||||||
ref: 38951317afe79d953ab25733667bd96e172a80d3
|
ref: 38951317afe79d953ab25733667bd96e172a80d3
|
||||||
uuid: ^3.0.7
|
uuid: ^3.0.7
|
||||||
auto_size_text_field: ^2.2.1
|
auto_size_text_field: ^2.2.1
|
||||||
flex_color_picker: ^3.3.0
|
flex_color_picker: ^3.3.0
|
||||||
dynamic_layouts:
|
dynamic_layouts:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/21pages/dynamic_layouts.git
|
url: https://github.com/rustdesk-org/dynamic_layouts.git
|
||||||
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
|
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
|
||||||
pull_down_button: ^0.9.3
|
pull_down_button: ^0.9.3
|
||||||
device_info_plus: ^9.1.0
|
device_info_plus: ^9.1.0
|
||||||
@@ -114,6 +114,9 @@ dev_dependencies:
|
|||||||
flutter_lints: ^2.0.2
|
flutter_lints: ^2.0.2
|
||||||
ffigen: ^8.0.2
|
ffigen: ^8.0.2
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
intl: ^0.19.0
|
||||||
|
|
||||||
# rerun: flutter pub run flutter_launcher_icons
|
# rerun: flutter pub run flutter_launcher_icons
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
image_path: "../res/icon.png"
|
image_path: "../res/icon.png"
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ flutter pub get
|
|||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
||||||
# call `flutter clean` if cargo build fails
|
# call `flutter clean` if cargo build fails
|
||||||
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
||||||
cargo build --features "flutter,flutter_texture_render"
|
cargo build --features flutter
|
||||||
flutter run $@
|
flutter run $@
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# RustDesk web
|
|
||||||
|
|
||||||
## Functions
|
|
||||||
|
|
||||||
### Current and planned
|
|
||||||
|
|
||||||
- [x] Outgoing
|
|
||||||
- [ ] Address book
|
|
||||||
- [ ] Force relay
|
|
||||||
|
|
||||||
### Unsupported
|
|
||||||
|
|
||||||
1. Incoming
|
|
||||||
2. LAN
|
|
||||||
3. Gpu texture render. We use WebGL instead.
|
|
||||||
|
|
||||||
### No plans
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg viewBox="0 0 375 375" style="width:32px;height:32px;margin:0 4px 4px 0" xmlns="http://www.w3.org/2000/svg"><rect transform="matrix(.91553 0 0 .91553 -152.92 116.76)" x="167.03" y="-127.54" width="409.6" height="409.6" rx="64" ry="64" fill="#0071ff"></rect><path d="M150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,19 +0,0 @@
|
|||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY = 'Desktop session not ready';
|
|
||||||
export const LOGIN_MSG_DESKTOP_XSESSION_FAILED = 'Desktop xsession failed';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER = 'Desktop session another user login';
|
|
||||||
export const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND = 'Desktop xorg not found';
|
|
||||||
// ls /usr/share/xsessions/
|
|
||||||
export const LOGIN_MSG_DESKTOP_NO_DESKTOP = 'Desktop none';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY =
|
|
||||||
'Desktop session not ready, password empty';
|
|
||||||
export const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG =
|
|
||||||
'Desktop session not ready, password wrong';
|
|
||||||
export const LOGIN_MSG_PASSWORD_EMPTY = 'Empty Password';
|
|
||||||
export const LOGIN_MSG_PASSWORD_WRONG = 'Wrong Password';
|
|
||||||
export const LOGIN_MSG_2FA_WRONG = 'Wrong 2FA Code';
|
|
||||||
export const REQUIRE_2FA = '2FA Required';
|
|
||||||
export const LOGIN_MSG_NO_PASSWORD_ACCESS = 'No Password Access';
|
|
||||||
export const LOGIN_MSG_OFFLINE = 'Offline';
|
|
||||||
export const LOGIN_SCREEN_WAYLAND = 'Wayland login screen is not supported';
|
|
||||||
export const SCRAP_X11_REQUIRED = 'x11 expected';
|
|
||||||
export const SCRAP_X11_REF_URL = 'https://rustdesk.com/docs/en/manual/linux/#x11-required';
|
|
||||||
@@ -1,792 +0,0 @@
|
|||||||
import Connection from "./connection";
|
|
||||||
import PORT from "./connection";
|
|
||||||
import _sodium from "libsodium-wrappers";
|
|
||||||
import { CursorData } from "./message";
|
|
||||||
import { loadVp9 } from "./codec";
|
|
||||||
import { checkIfRetry, version } from "./gen_js_from_hbb";
|
|
||||||
import { initZstd, translate } from "./common";
|
|
||||||
import PCMPlayer from "pcm-player";
|
|
||||||
|
|
||||||
window.curConn = undefined;
|
|
||||||
window.isMobile = () => {
|
|
||||||
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|
|
||||||
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDesktop() {
|
|
||||||
return !isMobile();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function msgbox(type, title, text, link) {
|
|
||||||
if (!type || (type == 'error' && !text)) return;
|
|
||||||
const text2 = text.toLowerCase();
|
|
||||||
var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
|
|
||||||
onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, link: link ?? '', hasRetry }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function jsonfyForDart(payload) {
|
|
||||||
var tmp = {};
|
|
||||||
for (const [key, value] of Object.entries(payload)) {
|
|
||||||
if (!key) continue;
|
|
||||||
if (value instanceof String || typeof value == 'string') {
|
|
||||||
tmp[key] = value;
|
|
||||||
} else if (value instanceof Uint8Array) {
|
|
||||||
tmp[key] = '[' + value.toString() + ']';
|
|
||||||
} else {
|
|
||||||
tmp[key] = JSON.stringify(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pushEvent(name, payload) {
|
|
||||||
payload = jsonfyForDart(payload);
|
|
||||||
payload.name = name;
|
|
||||||
onGlobalEvent(JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
let yuvWorker;
|
|
||||||
let yuvCanvas;
|
|
||||||
let gl;
|
|
||||||
let pixels;
|
|
||||||
let flipPixels;
|
|
||||||
let oldSize;
|
|
||||||
if (YUVCanvas.WebGLFrameSink.isAvailable()) {
|
|
||||||
var canvas = document.createElement('canvas');
|
|
||||||
yuvCanvas = YUVCanvas.attach(canvas, { webGL: true });
|
|
||||||
gl = canvas.getContext("webgl");
|
|
||||||
} else {
|
|
||||||
yuvWorker = new Worker("./yuv.js");
|
|
||||||
}
|
|
||||||
let testSpeed = [0, 0];
|
|
||||||
|
|
||||||
export function draw(display, frame) {
|
|
||||||
if (yuvWorker) {
|
|
||||||
// frame's (y/u/v).bytes already detached, can not transferrable any more.
|
|
||||||
yuvWorker.postMessage({ display, frame });
|
|
||||||
} else {
|
|
||||||
var tm0 = new Date().getTime();
|
|
||||||
yuvCanvas.drawFrame(frame);
|
|
||||||
var width = canvas.width;
|
|
||||||
var height = canvas.height;
|
|
||||||
var size = width * height * 4;
|
|
||||||
if (size != oldSize) {
|
|
||||||
pixels = new Uint8Array(size);
|
|
||||||
flipPixels = new Uint8Array(size);
|
|
||||||
oldSize = size;
|
|
||||||
}
|
|
||||||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
||||||
const row = width * 4;
|
|
||||||
const end = (height - 1) * row;
|
|
||||||
for (let i = 0; i < size; i += row) {
|
|
||||||
flipPixels.set(pixels.subarray(i, i + row), end - i);
|
|
||||||
}
|
|
||||||
onRgba(display, flipPixels);
|
|
||||||
testSpeed[1] += new Date().getTime() - tm0;
|
|
||||||
testSpeed[0] += 1;
|
|
||||||
if (testSpeed[0] > 30) {
|
|
||||||
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
|
|
||||||
testSpeed = [0, 0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
var testCanvas = document.getElementById("test-yuv-decoder-canvas");
|
|
||||||
if (testCanvas && currentFrame) {
|
|
||||||
var ctx = testCanvas.getContext("2d");
|
|
||||||
testCanvas.width = frame.format.displayWidth;
|
|
||||||
testCanvas.height = frame.format.displayHeight;
|
|
||||||
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
|
|
||||||
img.data.set(currentFrame);
|
|
||||||
ctx.putImageData(img, 0, 0);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendOffCanvas(c) {
|
|
||||||
let canvas = c.transferControlToOffscreen();
|
|
||||||
yuvWorker.postMessage({ canvas }, [canvas]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setConn(conn) {
|
|
||||||
window.curConn = conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConn() {
|
|
||||||
return window.curConn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function startConn(id) {
|
|
||||||
setByName('remote_id', id);
|
|
||||||
await curConn.start(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function close() {
|
|
||||||
getConn()?.close();
|
|
||||||
setConn(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function newConn() {
|
|
||||||
window.curConn?.close();
|
|
||||||
const conn = new Connection();
|
|
||||||
setConn(conn);
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sodium;
|
|
||||||
export async function verify(signed, pk) {
|
|
||||||
if (!sodium) {
|
|
||||||
await _sodium.ready;
|
|
||||||
sodium = _sodium;
|
|
||||||
}
|
|
||||||
if (typeof pk == 'string') {
|
|
||||||
pk = decodeBase64(pk);
|
|
||||||
}
|
|
||||||
return sodium.crypto_sign_open(signed, pk);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeBase64(pk) {
|
|
||||||
return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genBoxKeyPair() {
|
|
||||||
const pair = sodium.crypto_box_keypair();
|
|
||||||
const sk = pair.privateKey;
|
|
||||||
const pk = pair.publicKey;
|
|
||||||
return [sk, pk];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genSecretKey() {
|
|
||||||
return sodium.crypto_secretbox_keygen();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function seal(unsigned, theirPk, ourSk) {
|
|
||||||
const nonce = Uint8Array.from(Array(24).fill(0));
|
|
||||||
return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeOnce(value) {
|
|
||||||
var byteArray = Array(24).fill(0);
|
|
||||||
|
|
||||||
for (var index = 0; index < byteArray.length && value > 0; index++) {
|
|
||||||
var byte = value & 0xff;
|
|
||||||
byteArray[index] = byte;
|
|
||||||
value = (value - byte) / 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uint8Array.from(byteArray);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function encrypt(unsigned, nonce, key) {
|
|
||||||
return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decrypt(signed, nonce, key) {
|
|
||||||
return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.setByName = (name, value) => {
|
|
||||||
switch (name) {
|
|
||||||
case 'remote_id':
|
|
||||||
localStorage.setItem('remote-id', value);
|
|
||||||
break;
|
|
||||||
case 'connect':
|
|
||||||
newConn();
|
|
||||||
startConn(value);
|
|
||||||
break;
|
|
||||||
case 'login':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.setRemember(value.remember);
|
|
||||||
curConn.login({
|
|
||||||
os_login: {
|
|
||||||
username: value.os_username,
|
|
||||||
password: value.os_password,
|
|
||||||
},
|
|
||||||
password: value.password,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'close':
|
|
||||||
close();
|
|
||||||
break;
|
|
||||||
case 'refresh':
|
|
||||||
curConn.refresh();
|
|
||||||
break;
|
|
||||||
case 'reconnect':
|
|
||||||
curConn?.reconnect();
|
|
||||||
break;
|
|
||||||
case 'toggle_option':
|
|
||||||
curConn.toggleOption(value);
|
|
||||||
break;
|
|
||||||
case 'toggle_privacy_mode':
|
|
||||||
curConn.togglePrivacyMode(value);
|
|
||||||
break;
|
|
||||||
case 'image_quality':
|
|
||||||
curConn.setImageQuality(value);
|
|
||||||
break;
|
|
||||||
case 'lock_screen':
|
|
||||||
curConn.lockScreen();
|
|
||||||
break;
|
|
||||||
case 'ctrl_alt_del':
|
|
||||||
curConn.ctrlAltDel();
|
|
||||||
break;
|
|
||||||
case 'switch_display':
|
|
||||||
curConn.switchDisplay(value);
|
|
||||||
break;
|
|
||||||
case 'remove':
|
|
||||||
const peers = getPeers();
|
|
||||||
delete peers[value];
|
|
||||||
localStorage.setItem('peers', JSON.stringify(peers));
|
|
||||||
break;
|
|
||||||
case 'input_key':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
|
||||||
break;
|
|
||||||
case 'input_string':
|
|
||||||
curConn.inputString(value);
|
|
||||||
break;
|
|
||||||
case 'send_mouse':
|
|
||||||
if (!curConn) return;
|
|
||||||
let mask = 0;
|
|
||||||
value = JSON.parse(value);
|
|
||||||
switch (value.type) {
|
|
||||||
case 'down':
|
|
||||||
mask = 1;
|
|
||||||
break;
|
|
||||||
case 'up':
|
|
||||||
mask = 2;
|
|
||||||
break;
|
|
||||||
case 'wheel':
|
|
||||||
mask = 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (value.buttons) {
|
|
||||||
case 'left':
|
|
||||||
mask |= 1 << 3;
|
|
||||||
break;
|
|
||||||
case 'right':
|
|
||||||
mask |= 2 << 3;
|
|
||||||
break;
|
|
||||||
case 'wheel':
|
|
||||||
mask |= 4 << 3;
|
|
||||||
}
|
|
||||||
curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
|
||||||
break;
|
|
||||||
case 'send_2fa':
|
|
||||||
curConn.send2fa(value);
|
|
||||||
break;
|
|
||||||
case 'option':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
localStorage.setItem(value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'options':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
for (const [key, value] of Object.entries(value)) {
|
|
||||||
localStorage.setItem(key, value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'option:local':
|
|
||||||
case 'option:flutter:local':
|
|
||||||
case 'option:flutter:peer':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
localStorage.setItem(name + ':' + value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'option:user:default':
|
|
||||||
setUserDefaultOption(value);
|
|
||||||
break;
|
|
||||||
case 'option:session':
|
|
||||||
value = JSON.parse(value);
|
|
||||||
curConn.setOption(value.name, value.value);
|
|
||||||
break;
|
|
||||||
case 'option:peer':
|
|
||||||
setPeerOption(value);
|
|
||||||
break;
|
|
||||||
case 'option:toggle':
|
|
||||||
return curConn.toggleOption(value);
|
|
||||||
case 'input_os_password':
|
|
||||||
curConn.inputOsPassword(value);
|
|
||||||
break;
|
|
||||||
case 'session_add_sync':
|
|
||||||
return sessionAdd(value);
|
|
||||||
case 'session_start':
|
|
||||||
sessionStart(value);
|
|
||||||
break;
|
|
||||||
case 'session_close':
|
|
||||||
sessionClose(value);
|
|
||||||
break;
|
|
||||||
case 'elevate_with_logon':
|
|
||||||
curConn.elevateWithLogon(value);
|
|
||||||
break;
|
|
||||||
case 'forget':
|
|
||||||
curConn.setRemember(false);
|
|
||||||
break;
|
|
||||||
case 'peer_has_password':
|
|
||||||
const options = getPeers()[value] || {};
|
|
||||||
return (options['password'] ?? '') !== '';
|
|
||||||
case 'peer_exists':
|
|
||||||
return !(!getPeers()[value]);
|
|
||||||
case 'restart':
|
|
||||||
curConn.restart();
|
|
||||||
break;
|
|
||||||
case 'fav':
|
|
||||||
return localStorage.setItem('fav', value);
|
|
||||||
case 'query_onlines':
|
|
||||||
queryOnlines(value);
|
|
||||||
break;
|
|
||||||
case 'change_prefer_codec':
|
|
||||||
curConn.changePreferCodec(value);
|
|
||||||
case 'cursor':
|
|
||||||
setCustomCursor(value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.getByName = (name, arg) => {
|
|
||||||
let v = _getByName(name, arg);
|
|
||||||
if (typeof v == 'string' || v instanceof String) return v;
|
|
||||||
if (v == undefined || v == null) return '';
|
|
||||||
return JSON.stringify(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getByName(name, arg) {
|
|
||||||
switch (name) {
|
|
||||||
case 'remote_id':
|
|
||||||
return localStorage.getItem('remote-id');
|
|
||||||
case 'remember':
|
|
||||||
return curConn.getRemember();
|
|
||||||
case 'toggle_option':
|
|
||||||
return curConn.getOption(arg) || false;
|
|
||||||
case 'option':
|
|
||||||
return localStorage.getItem(arg);
|
|
||||||
case 'options':
|
|
||||||
const keys = [
|
|
||||||
'custom-rendezvous-server',
|
|
||||||
'relay-server',
|
|
||||||
'api-server',
|
|
||||||
'key'
|
|
||||||
];
|
|
||||||
const obj = {};
|
|
||||||
keys.forEach(key => {
|
|
||||||
const v = localStorage.getItem(key);
|
|
||||||
if (v) obj[key] = v;
|
|
||||||
});
|
|
||||||
return JSON.stringify(obj);
|
|
||||||
case 'option:local':
|
|
||||||
case 'option:flutter:local':
|
|
||||||
case 'option:flutter:peer':
|
|
||||||
return localStorage.getItem(name + ':' + arg);
|
|
||||||
case 'image_quality':
|
|
||||||
return curConn.getImageQuality();
|
|
||||||
case 'translate':
|
|
||||||
arg = JSON.parse(arg);
|
|
||||||
return translate(arg.locale, arg.text);
|
|
||||||
case 'option:user:default':
|
|
||||||
return getUserDefaultOption(arg);
|
|
||||||
case 'option:session':
|
|
||||||
if (curConn) {
|
|
||||||
return curConn.getOption(arg);
|
|
||||||
} else {
|
|
||||||
return getUserDefaultOption(arg);
|
|
||||||
}
|
|
||||||
case 'option:peer':
|
|
||||||
return getPeerOption(arg);
|
|
||||||
case 'option:toggle':
|
|
||||||
return curConn.getToggleOption(arg);
|
|
||||||
case 'get_conn_status':
|
|
||||||
if (curConn) {
|
|
||||||
return curConn.getStatus();
|
|
||||||
} else {
|
|
||||||
return JSON.stringify({ status_num: 0 });
|
|
||||||
}
|
|
||||||
case 'test_if_valid_server':
|
|
||||||
break;
|
|
||||||
case 'version':
|
|
||||||
return version;
|
|
||||||
case 'load_recent_peers':
|
|
||||||
loadRecentPeers();
|
|
||||||
break;
|
|
||||||
case 'load_fav_peers':
|
|
||||||
loadFavPeers();
|
|
||||||
break;
|
|
||||||
case 'fav':
|
|
||||||
return localStorage.getItem('fav') ?? '[]';
|
|
||||||
case 'load_recent_peers_sync':
|
|
||||||
return JSON.stringify({
|
|
||||||
peers: JSON.stringify(getRecentPeers())
|
|
||||||
});
|
|
||||||
case 'api_server':
|
|
||||||
return getApiServer();
|
|
||||||
case 'is_using_public_server':
|
|
||||||
return !localStorage.getItem('custom-rendezvous-server');
|
|
||||||
case 'get_version_number':
|
|
||||||
return getVersionNumber(arg);
|
|
||||||
case 'audit_server':
|
|
||||||
return getAuditServer(arg);
|
|
||||||
case 'alternative_codecs':
|
|
||||||
return getAlternativeCodecs();
|
|
||||||
case 'screen_info':
|
|
||||||
return JSON.stringify({
|
|
||||||
frame: {
|
|
||||||
l: window.screenX,
|
|
||||||
t: window.screenY,
|
|
||||||
r: window.screenX + window.innerWidth,
|
|
||||||
b: window.screenY + window.innerHeight,
|
|
||||||
},
|
|
||||||
visibleFrame: {
|
|
||||||
l: window.screen.availLeft,
|
|
||||||
t: window.screen.availTop,
|
|
||||||
r: window.screen.availLeft + window.screen.availWidth,
|
|
||||||
b: window.screen.availTop + window.screen.availHeight,
|
|
||||||
},
|
|
||||||
scaleFactor: window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
case 'main_display':
|
|
||||||
return JSON.stringify({
|
|
||||||
w: window.screen.availWidth,
|
|
||||||
h: window.screen.availHeight,
|
|
||||||
scaleFactor: window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
let opusWorker = new Worker("./libopus.js");
|
|
||||||
let pcmPlayer;
|
|
||||||
|
|
||||||
export function initAudio(channels, sampleRate) {
|
|
||||||
pcmPlayer = newAudioPlayer(channels, sampleRate);
|
|
||||||
opusWorker.postMessage({ channels, sampleRate });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function playAudio(packet) {
|
|
||||||
opusWorker.postMessage(packet, [packet.buffer]);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.init = async () => {
|
|
||||||
if (yuvWorker) {
|
|
||||||
yuvWorker.onmessage = (e) => {
|
|
||||||
onRgba(e.data.display, e.data.frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opusWorker.onmessage = (e) => {
|
|
||||||
pcmPlayer.feed(e.data);
|
|
||||||
}
|
|
||||||
loadVp9(() => { });
|
|
||||||
await initZstd();
|
|
||||||
console.log('init done');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPeers() {
|
|
||||||
return getJsonObj('peers');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getJsonObj(key) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem(key)) || {};
|
|
||||||
} catch (e) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function newAudioPlayer(channels, sampleRate) {
|
|
||||||
return new PCMPlayer({
|
|
||||||
channels,
|
|
||||||
sampleRate,
|
|
||||||
flushingTime: 2000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function copyToClipboard(text) {
|
|
||||||
if (window.clipboardData && window.clipboardData.setData) {
|
|
||||||
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
|
|
||||||
return window.clipboardData.setData("Text", text);
|
|
||||||
}
|
|
||||||
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
|
|
||||||
var textarea = document.createElement("textarea");
|
|
||||||
textarea.textContent = text;
|
|
||||||
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
try {
|
|
||||||
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.warn("Copy to clipboard failed.", ex);
|
|
||||||
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dup to the function in hbb_common, lib.rs
|
|
||||||
// Maybe we need to move this function to js part.
|
|
||||||
export function getVersionNumber(v) {
|
|
||||||
try {
|
|
||||||
let versions = v.split('-');
|
|
||||||
|
|
||||||
let n = 0;
|
|
||||||
|
|
||||||
// The first part is the version number.
|
|
||||||
// 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10
|
|
||||||
// to leave space for patch version.
|
|
||||||
if (versions.length > 0) {
|
|
||||||
let last = 0;
|
|
||||||
for (let x of versions[0].split('.')) {
|
|
||||||
last = parseInt(x) || 0;
|
|
||||||
n = n * 1000 + last;
|
|
||||||
}
|
|
||||||
n -= last;
|
|
||||||
n += last * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (versions.length > 1) {
|
|
||||||
n += parseInt(versions[1]) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore the rest
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to parse version number: "' + v + '" ' + e.message);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the cursor for the flutter-view element
|
|
||||||
function setCustomCursor(value) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(value);
|
|
||||||
// document querySelector or evaluate can not find the custom element
|
|
||||||
var body = document.body;
|
|
||||||
for (var i = 0; i < body.children.length; i++) {
|
|
||||||
var child = body.children[i];
|
|
||||||
if (child.tagName == 'FLUTTER-VIEW') {
|
|
||||||
child.style.cursor = `url(${obj.url}) ${obj.hotx} ${obj.hoty}, auto`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to set custom cursor: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================== options begin ==========================
|
|
||||||
function setUserDefaultOption(value) {
|
|
||||||
try {
|
|
||||||
const ojb = JSON.parse(value);
|
|
||||||
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
|
|
||||||
userDefaultOptions[ojb.name] = ojb.value;
|
|
||||||
localStorage.setItem('user-default-options', JSON.stringify(userDefaultOptions));
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to set user default options: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserDefaultOption(value) {
|
|
||||||
const defaultOptions = {
|
|
||||||
'view_style': 'original',
|
|
||||||
'scroll_style': 'scrollauto',
|
|
||||||
'image_quality': 'balanced',
|
|
||||||
'codec-preference': 'auto',
|
|
||||||
'custom_image_quality': '50',
|
|
||||||
'custom-fps': '30',
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const userDefaultOptions = JSON.parse(localStorage.getItem('user-default-options')) || {};
|
|
||||||
return userDefaultOptions[value] || defaultOptions[value] || '';
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to get user default options: ' + e.message);
|
|
||||||
return defaultOptions[value] || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPeerOption(value) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(value);
|
|
||||||
const options = getPeers()[obj.id] || {};
|
|
||||||
return options[obj.name] ?? getUserDefaultOption(obj.name);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to get peer option: "' + value + '", ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPeerOption(param) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(param);
|
|
||||||
const id = obj.id;
|
|
||||||
const name = obj.name;
|
|
||||||
const value = obj.value;
|
|
||||||
const peers = getPeers();
|
|
||||||
const options = peers[id] || {};
|
|
||||||
|
|
||||||
if (value == undefined) {
|
|
||||||
delete options[name];
|
|
||||||
} else {
|
|
||||||
options[name] = value;
|
|
||||||
}
|
|
||||||
options["tm"] = new Date().getTime();
|
|
||||||
peers[id] = options;
|
|
||||||
localStorage.setItem("peers", JSON.stringify(peers));
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Failed to set peer option: "' + value + '", ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ========================= options end ===========================
|
|
||||||
|
|
||||||
// ========================== peers begin ==========================
|
|
||||||
function getRecentPeers() {
|
|
||||||
const peers = [];
|
|
||||||
for (const [id, value] of Object.entries(getPeers())) {
|
|
||||||
if (!id) continue;
|
|
||||||
const tm = value['tm'];
|
|
||||||
const info = value['info'];
|
|
||||||
const cardInfo = {
|
|
||||||
id: id,
|
|
||||||
username: info['username'] || '',
|
|
||||||
hostname: info['hostname'] || '',
|
|
||||||
platform: info['platform'] || '',
|
|
||||||
alias: value.alias || '',
|
|
||||||
};
|
|
||||||
if (!tm || !cardInfo) continue;
|
|
||||||
peers.push([tm, id, cardInfo]);
|
|
||||||
}
|
|
||||||
return peers.sort().reverse().map(x => x[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadRecentPeers() {
|
|
||||||
const peersRecent = getRecentPeers();
|
|
||||||
if (peersRecent) {
|
|
||||||
onRegisteredEvent(JSON.stringify({ name: 'load_recent_peers', peers: JSON.stringify(peersRecent) }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadFavPeers() {
|
|
||||||
try {
|
|
||||||
const fav = localStorage.getItem('fav') ?? '[]';
|
|
||||||
const favs = JSON.parse(fav);
|
|
||||||
const peersFav = getRecentPeers().filter(x => favs.includes(x.id));
|
|
||||||
if (peersFav) {
|
|
||||||
onRegisteredEvent(JSON.stringify({ name: 'load_fav_peers', peers: JSON.stringify(peersFav) }));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load fav peers: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queryOnlines(value) {
|
|
||||||
// TODO: implement this
|
|
||||||
}
|
|
||||||
// ========================== peers end ===========================
|
|
||||||
|
|
||||||
// ========================== session begin ==========================
|
|
||||||
function sessionAdd(value) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(value);
|
|
||||||
window.curConn?.close();
|
|
||||||
const conn = new Connection();
|
|
||||||
setConn(conn);
|
|
||||||
return '';
|
|
||||||
} catch (e) {
|
|
||||||
return e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionStart(value) {
|
|
||||||
try {
|
|
||||||
const conn = getConn();
|
|
||||||
if (!conn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(value);
|
|
||||||
if (data['id']) {
|
|
||||||
startConn(data['id']);
|
|
||||||
} else {
|
|
||||||
msgbox('error', 'Error', 'No id found in session data ' + value, '');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: better error handling
|
|
||||||
msgbox('error', 'Error', e.message, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionClose(value) {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
// ========================== session end ===========================
|
|
||||||
|
|
||||||
// ========================== settings begin ==========================
|
|
||||||
function increasePort(host, offset) {
|
|
||||||
function isIPv6(str) {
|
|
||||||
const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$/;
|
|
||||||
return ipv6Pattern.test(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isIPv6(host)) {
|
|
||||||
if (host.startsWith('[')) {
|
|
||||||
let tmp = host.split(']:');
|
|
||||||
if (tmp.length === 2) {
|
|
||||||
let port = parseInt(tmp[1]) || 0;
|
|
||||||
if (port > 0) {
|
|
||||||
return `${tmp[0]}]:${port + offset}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (host.includes(':')) {
|
|
||||||
let tmp = host.split(':');
|
|
||||||
if (tmp.length === 2) {
|
|
||||||
let port = parseInt(tmp[1]) || 0;
|
|
||||||
if (port > 0) {
|
|
||||||
return `${tmp[0]}:${port + offset}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAlternativeCodecs() {
|
|
||||||
return JSON.stringify({
|
|
||||||
vp8: true,
|
|
||||||
av1: false,
|
|
||||||
h264: false,
|
|
||||||
h265: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// ========================== settings end ===========================
|
|
||||||
|
|
||||||
// ========================== server begin ==========================
|
|
||||||
function getApiServer() {
|
|
||||||
const api_server = localStorage.getItem('api-server');
|
|
||||||
if (api_server) {
|
|
||||||
return api_server;
|
|
||||||
}
|
|
||||||
|
|
||||||
const custom_rendezvous_server = localStorage.getItem('custom-rendezvous-server');
|
|
||||||
if (custom_rendezvous_server) {
|
|
||||||
let s = increasePort(custom_rendezvous_server, -2);
|
|
||||||
if (s == custom_rendezvous_server) {
|
|
||||||
return `http://${s}:${PORT - 2}`;
|
|
||||||
} else {
|
|
||||||
return `http://${s}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'https://admin.rustdesk.com';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuditServer(typ) {
|
|
||||||
if (!localStorage.getItem("access_token")) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const api_server = getApiServer();
|
|
||||||
if (!api_server || api_server.includes('rustdesk.com')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return api_server + '/api/audit/' + typ;
|
|
||||||
}
|
|
||||||
// ========================== server end ============================
|
|
||||||
1
flutter/web/v1/README.md
Normal file
1
flutter/web/v1/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v1 is not compatible with current Flutter source code.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user