mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-20 15:55:08 +08:00
Compare commits
434 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a516f01feb | ||
|
|
2e314bf032 | ||
|
|
b93d4ce3fc | ||
|
|
21bcfd173d | ||
|
|
3d5262c36f | ||
|
|
cfd801c5d6 | ||
|
|
216a72592d | ||
|
|
ddd3401bd7 | ||
|
|
47139edd81 | ||
|
|
c6e3f60a6b | ||
|
|
88a99211f3 | ||
|
|
d08c335fdf | ||
|
|
e5ec6957fe | ||
|
|
e20f5dd001 | ||
|
|
e1a6ccc100 | ||
|
|
cc288272d3 | ||
|
|
49ce4edb8a | ||
|
|
29c3b29bda | ||
|
|
8a8f708c3e | ||
|
|
c5038b1a78 | ||
|
|
f4c038ea93 | ||
|
|
d9ea717056 | ||
|
|
40af9dc78b | ||
|
|
81fc22a156 | ||
|
|
2e7bd26e4c | ||
|
|
179b562472 | ||
|
|
ab246fdcbf | ||
|
|
d65d3b7326 | ||
|
|
9f9a22ec63 | ||
|
|
a8f1a66043 | ||
|
|
0b3e7bf33e | ||
|
|
c358399eca | ||
|
|
cacca7295c | ||
|
|
d2e98cc620 | ||
|
|
2e81bcb447 | ||
|
|
cbca0eb340 | ||
|
|
9380f33d7c | ||
|
|
519539ed0a | ||
|
|
1f2a75fbd8 | ||
|
|
51055a7e5b | ||
|
|
13effe7f14 | ||
|
|
943f96ef8c | ||
|
|
260a82ee5c | ||
|
|
a2792d1527 | ||
|
|
2922ebe22a | ||
|
|
1e6944b380 | ||
|
|
993862c103 | ||
|
|
c8cd564e69 | ||
|
|
a4cd64f0d5 | ||
|
|
f0ca4b9fee | ||
|
|
aa3402b44a | ||
|
|
26ebd0deb9 | ||
|
|
4150036589 | ||
|
|
7a1157f1b0 | ||
|
|
3bd34bf0b9 | ||
|
|
5f29016861 | ||
|
|
e40243b55d | ||
|
|
dbbbd08934 | ||
|
|
29e12b84a9 | ||
|
|
04c0f66ca9 | ||
|
|
ec28567362 | ||
|
|
d4377a13c5 | ||
|
|
39e713838f | ||
|
|
75a4671bda | ||
|
|
827efabbc0 | ||
|
|
532fe6aefb | ||
|
|
ae339f039d | ||
|
|
bf390611ab | ||
|
|
e3f6829d02 | ||
|
|
832002a10f | ||
|
|
d335cdbb0c | ||
|
|
6a5d5875c8 | ||
|
|
cf06d1028f | ||
|
|
fd178a7b6c | ||
|
|
f3a2733d75 | ||
|
|
55de573a01 | ||
|
|
40239a1c41 | ||
|
|
c68ce7dd84 | ||
|
|
690a2c8399 | ||
|
|
4b4fd94f3e | ||
|
|
5abe42f66c | ||
|
|
48aec6484c | ||
|
|
a946d4d0c9 | ||
|
|
24f4b94082 | ||
|
|
aa1e122532 | ||
|
|
d400999b9c | ||
|
|
1d416f6626 | ||
|
|
9d9741f18e | ||
|
|
50aa8e12ad | ||
|
|
5931af460e | ||
|
|
fc607d6789 | ||
|
|
529e70910d | ||
|
|
f300d797e2 | ||
|
|
e3cce2824d | ||
|
|
f34b8411a7 | ||
|
|
8745fcbb6a | ||
|
|
2a0fd55af7 | ||
|
|
da70cbcdda | ||
|
|
921b64e1e0 | ||
|
|
c0de0aa108 | ||
|
|
715d475f49 | ||
|
|
e3f09b3ec6 | ||
|
|
0a5fafb84f | ||
|
|
4e084c5ee0 | ||
|
|
d1fe617670 | ||
|
|
7744bdbbe0 | ||
|
|
e1329c8157 | ||
|
|
f31e60af5b | ||
|
|
ed18e3c786 | ||
|
|
579e0fac36 | ||
|
|
92752765ba | ||
|
|
071f51cf6f | ||
|
|
dde3cce120 | ||
|
|
bb1b9858d5 | ||
|
|
a31c27be73 | ||
|
|
85ae3916cb | ||
|
|
cc9b7e64eb | ||
|
|
5e22a49e49 | ||
|
|
07cf1b4db5 | ||
|
|
f6ab5cdcb2 | ||
|
|
b477aded0b | ||
|
|
65318efd67 | ||
|
|
dbd195a46e | ||
|
|
0651ad492f | ||
|
|
a3c5adb1f4 | ||
|
|
a771abcdc2 | ||
|
|
b8b3a089f3 | ||
|
|
8f00067266 | ||
|
|
83bf067d18 | ||
|
|
1729ee337f | ||
|
|
57834840b8 | ||
|
|
99d7b62d79 | ||
|
|
6625aca994 | ||
|
|
ce56be6507 | ||
|
|
fd69b14623 | ||
|
|
7521bbe15f | ||
|
|
3c6ddd7403 | ||
|
|
6820e2f4c7 | ||
|
|
77f3ebaf1a | ||
|
|
e7e244d4f2 | ||
|
|
f4c40d733e | ||
|
|
049c334db3 | ||
|
|
6197832317 | ||
|
|
2fd53f9825 | ||
|
|
ae16b8975b | ||
|
|
171177c76f | ||
|
|
ade1d8c0c7 | ||
|
|
9a194f0850 | ||
|
|
76d5a8b205 | ||
|
|
bc6ce6c7ee | ||
|
|
025cdfa25b | ||
|
|
2f432e941d | ||
|
|
96edca8f74 | ||
|
|
51b250435d | ||
|
|
5a2121501d | ||
|
|
877b3e2ce5 | ||
|
|
421ddc0016 | ||
|
|
2662abc5a3 | ||
|
|
b3e1c8a907 | ||
|
|
2266fde26f | ||
|
|
e58e75eea9 | ||
|
|
eafebdba21 | ||
|
|
7bf5e69444 | ||
|
|
cb0dc46d08 | ||
|
|
9b8209b61b | ||
|
|
b6ba9978e3 | ||
|
|
0d1d7a9b87 | ||
|
|
508dd5b383 | ||
|
|
31a1b7a80b | ||
|
|
ba43424781 | ||
|
|
f899b2a962 | ||
|
|
2dd3d8c11e | ||
|
|
6eea425280 | ||
|
|
5e7d4fd2d6 | ||
|
|
d9fba50606 | ||
|
|
b6035fbbdf | ||
|
|
e67b694f06 | ||
|
|
2333ee2c07 | ||
|
|
61ccc2152e | ||
|
|
f6aca4ca8e | ||
|
|
1707987a7b | ||
|
|
85604dee79 | ||
|
|
a12969be30 | ||
|
|
9f91eada89 | ||
|
|
cb6a6aa42a | ||
|
|
4fec8abad4 | ||
|
|
35571dc8d7 | ||
|
|
a103b83647 | ||
|
|
e1e4bf599b | ||
|
|
cba8aaa410 | ||
|
|
8ced4ddaa2 | ||
|
|
15404ecab4 | ||
|
|
97772f9ac5 | ||
|
|
764fbe2c9d | ||
|
|
e03344d85b | ||
|
|
0e98a51775 | ||
|
|
0a1d3c4afb | ||
|
|
fd9b5f3c57 | ||
|
|
73f6afd4c0 | ||
|
|
50dd2b3aad | ||
|
|
c0e9445602 | ||
|
|
541d9c6b86 | ||
|
|
8c91e5c5ca | ||
|
|
19d1605d8c | ||
|
|
0faf82f109 | ||
|
|
ee5314de20 | ||
|
|
7e8d3bd2ac | ||
|
|
9750e1409c | ||
|
|
67d4e061fb | ||
|
|
f67f2be0cb | ||
|
|
aa42bf548e | ||
|
|
d679e8fa7d | ||
|
|
1357ef5d6f | ||
|
|
30a5d1e0e1 | ||
|
|
9f0985c842 | ||
|
|
be06c0d738 | ||
|
|
f0f50f0f03 | ||
|
|
1850d32f49 | ||
|
|
3999d498be | ||
|
|
bbdce8d57b | ||
|
|
baf70da2fe | ||
|
|
b967d496cc | ||
|
|
2aef79688b | ||
|
|
0451a1c45f | ||
|
|
f7e9057a39 | ||
|
|
d73e0e1e5a | ||
|
|
39dbd89287 | ||
|
|
c04f460bbd | ||
|
|
79a1f888d6 | ||
|
|
57d1b1ecc4 | ||
|
|
8a4a2b5732 | ||
|
|
51a60a7eed | ||
|
|
b26acde450 | ||
|
|
2de81045ea | ||
|
|
a72a8906b0 | ||
|
|
614086a216 | ||
|
|
2ffc2ad85b | ||
|
|
eef091d4e8 | ||
|
|
97f26f880b | ||
|
|
22c6f5e589 | ||
|
|
b828768fa9 | ||
|
|
31e7b6acf1 | ||
|
|
14b505130b | ||
|
|
22f3425ace | ||
|
|
4723d6a830 | ||
|
|
c4f3c0f133 | ||
|
|
de375c91bb | ||
|
|
d3454f07d3 | ||
|
|
cf8ef2533a | ||
|
|
6ad662260e | ||
|
|
2b54a553c7 | ||
|
|
85ded0a3e5 | ||
|
|
5c16a8302e | ||
|
|
04c175c62e | ||
|
|
2be05608d8 | ||
|
|
c3c99ba107 | ||
|
|
a81d6468cc | ||
|
|
48464835f5 | ||
|
|
edc5d86ee7 | ||
|
|
e9c8ba5393 | ||
|
|
a72bc0fb28 | ||
|
|
5a8c8cbf7c | ||
|
|
b68d7a3054 | ||
|
|
9e931a6f04 | ||
|
|
f0587796e2 | ||
|
|
875ac28ab5 | ||
|
|
6821bef5e5 | ||
|
|
930561f431 | ||
|
|
bc672b3367 | ||
|
|
e283d33f28 | ||
|
|
901505e8be | ||
|
|
a4565bf0da | ||
|
|
092e4089c7 | ||
|
|
188f85b042 | ||
|
|
72c96f22b6 | ||
|
|
0143eaf601 | ||
|
|
09466680d3 | ||
|
|
eec879a801 | ||
|
|
3f11d9cdb6 | ||
|
|
8512c2b2b0 | ||
|
|
e2a7e38a39 | ||
|
|
3a0ece1447 | ||
|
|
d0a54a6cc6 | ||
|
|
bed214bd37 | ||
|
|
5f31211db3 | ||
|
|
29b8875c1c | ||
|
|
3367c541b2 | ||
|
|
30afe4f779 | ||
|
|
d18e95703e | ||
|
|
9adc083def | ||
|
|
0dc664474a | ||
|
|
5e8fe239fa | ||
|
|
7a3100a87c | ||
|
|
8a1acedae5 | ||
|
|
f5bcc17636 | ||
|
|
883c630206 | ||
|
|
a95a6ab733 | ||
|
|
46605fab1b | ||
|
|
9d26fec631 | ||
|
|
294a6ce9bc | ||
|
|
183ea47ba4 | ||
|
|
06e04143a8 | ||
|
|
a532b36e28 | ||
|
|
c873b69662 | ||
|
|
b30f84623b | ||
|
|
888e993534 | ||
|
|
1d59a7fe5f | ||
|
|
2c027cdcf5 | ||
|
|
fe513dd967 | ||
|
|
d652b99d5b | ||
|
|
c2716c2509 | ||
|
|
821f7245b0 | ||
|
|
0ea88ce6ff | ||
|
|
21f41e98a0 | ||
|
|
282ea02ebf | ||
|
|
170200fa49 | ||
|
|
d8cee6507d | ||
|
|
2391b18046 | ||
|
|
b5a7165015 | ||
|
|
ef4d84657b | ||
|
|
011647511c | ||
|
|
e2d217a138 | ||
|
|
f07936a911 | ||
|
|
0bb4d43e9e | ||
|
|
6f74080a2d | ||
|
|
8a370e640a | ||
|
|
d007408061 | ||
|
|
02572e9032 | ||
|
|
af66d2a73b | ||
|
|
eb5ab4d7d9 | ||
|
|
c02b4f994a | ||
|
|
d093fdc256 | ||
|
|
2af799f46e | ||
|
|
3c7e24c605 | ||
|
|
7d961d895b | ||
|
|
53dbc2fa6f | ||
|
|
024220e58a | ||
|
|
8621b93436 | ||
|
|
ac88121c4a | ||
|
|
90df80ed78 | ||
|
|
d4f3a87276 | ||
|
|
48efdcf1f0 | ||
|
|
0511cdbb21 | ||
|
|
8747b9847f | ||
|
|
92d0fe1c3f | ||
|
|
a9015bcf70 | ||
|
|
f8f2686267 | ||
|
|
c2bd1b8965 | ||
|
|
4eeee5b7ee | ||
|
|
dfc224ec01 | ||
|
|
86ff768241 | ||
|
|
94addb162b | ||
|
|
bea65f8739 | ||
|
|
92f570831d | ||
|
|
9349210a87 | ||
|
|
95f4274eca | ||
|
|
a6febb2816 | ||
|
|
e294dafe7c | ||
|
|
d00582e929 | ||
|
|
6d2e985593 | ||
|
|
182e8c4ac0 | ||
|
|
40019b80f6 | ||
|
|
2f40b9dc04 | ||
|
|
8602b036bd | ||
|
|
51db8e706d | ||
|
|
a0dc38f749 | ||
|
|
625b610cfd | ||
|
|
62a8349739 | ||
|
|
0ab500c27c | ||
|
|
285e974d1a | ||
|
|
e71d86c124 | ||
|
|
14343e89d4 | ||
|
|
3f2dfa521c | ||
|
|
cd73368cb9 | ||
|
|
84b5cd70ed | ||
|
|
01672bc697 | ||
|
|
763174657b | ||
|
|
15fa80fb26 | ||
|
|
d537e2563d | ||
|
|
1719e478e3 | ||
|
|
1f129e6ef3 | ||
|
|
25d0ced8ba | ||
|
|
2116fec20b | ||
|
|
1252f45506 | ||
|
|
1f4c62e480 | ||
|
|
bd334769fa | ||
|
|
750368af7b | ||
|
|
2fb35c3596 | ||
|
|
5114a9d369 | ||
|
|
4b6ba7938f | ||
|
|
1e400d2a64 | ||
|
|
967e63266f | ||
|
|
f9b0a88213 | ||
|
|
d67afa49b4 | ||
|
|
1fd170b089 | ||
|
|
a632718e80 | ||
|
|
9f72d05749 | ||
|
|
c062813c6d | ||
|
|
3ae1638125 | ||
|
|
96aff38862 | ||
|
|
ed3fb1efa4 | ||
|
|
d689bbf38e | ||
|
|
c1bbdaf9ae | ||
|
|
ab9e1013b2 | ||
|
|
e1140b1bea | ||
|
|
cfd27c8d87 | ||
|
|
a18947eed2 | ||
|
|
f8592e0d5b | ||
|
|
5bfdf05ff2 | ||
|
|
9e851542ec | ||
|
|
e79946b4e4 | ||
|
|
aed212d8f8 | ||
|
|
c5d3c7f390 | ||
|
|
b047730830 | ||
|
|
9c7d4ef1f7 | ||
|
|
12d3c59172 | ||
|
|
ef06b7d5d0 | ||
|
|
f17e17a6b9 | ||
|
|
faf363cfd2 | ||
|
|
dbbd9179b7 | ||
|
|
49f848a453 | ||
|
|
ef56aea74f | ||
|
|
cb5fa85ac2 | ||
|
|
11bdd3cfcd | ||
|
|
f0dcc91907 | ||
|
|
c1c2d26ec7 | ||
|
|
93133b9a6c | ||
|
|
245f08055f | ||
|
|
00ddd63372 | ||
|
|
1765c7bbf4 | ||
|
|
65edd55516 | ||
|
|
4947cf8718 | ||
|
|
65dd2b8993 | ||
|
|
ef82cfa034 |
@@ -1,8 +1,14 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||
[target.i686-pc-windows-msvc]
|
||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
|
||||
[target.'cfg(target_os="macos")']
|
||||
rustflags = [
|
||||
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
|
||||
]
|
||||
#[target.'cfg(target_os="linux")']
|
||||
# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
|
||||
# this is unlikely to help also, because the other so files still use libc dynamically
|
||||
#rustflags = [
|
||||
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
|
||||
#]
|
||||
|
||||
2
.github/workflows/bridge.yml
vendored
2
.github/workflows/bridge.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
FLUTTER_VERSION: "3.19.6"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -4,9 +4,9 @@ env:
|
||||
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2023.10.19
|
||||
# vcpkg version: 2024.06.15
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -112,6 +112,8 @@ jobs:
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
|
||||
39
.github/workflows/fdroid.yml
vendored
Normal file
39
.github/workflows/fdroid.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Fdroid version file generation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||
- '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||
|
||||
jobs:
|
||||
# https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml
|
||||
# Finds latest release and transforms F-Droid version code from version as follows:
|
||||
# X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A
|
||||
update-fdroid-version-file:
|
||||
name: Publish RustDesk version file for F-Droid updater
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate RustDesk version file
|
||||
run: |
|
||||
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||
UPSTREAM_VERNAME="${GITHUB_REF##refs/tags/}"
|
||||
UPSTREAM_VERNAME="${UPSTREAM_VERNAME##v}"
|
||||
else
|
||||
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
|
||||
fi
|
||||
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
|
||||
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
||||
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
||||
shell: bash
|
||||
|
||||
- name: Publish RustDesk version file
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: "fdroid-version"
|
||||
files: |
|
||||
./rustdesk-version.txt
|
||||
653
.github/workflows/flutter-build.yml
vendored
653
.github/workflows/flutter-build.yml
vendored
File diff suppressed because it is too large
Load Diff
210
.github/workflows/playground.yml
vendored
210
.github/workflows/playground.yml
vendored
@@ -10,15 +10,15 @@ 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.13.9"
|
||||
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.03.25
|
||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
||||
VERSION: "1.2.6"
|
||||
# vcpkg version: 2024.06.15
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
VERSION: "1.3.1"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -31,7 +31,207 @@ env:
|
||||
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:
|
||||
@@ -62,7 +262,7 @@ jobs:
|
||||
git \
|
||||
g++ \
|
||||
g++-multilib \
|
||||
libappindicator3-dev \
|
||||
libayatana-appindicator3-dev\
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-10-dev \
|
||||
|
||||
2
.github/workflows/winget.yml
vendored
2
.github/workflows/winget.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
|
||||
2720
Cargo.lock
generated
2720
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.2.6"
|
||||
version = "1.3.1"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -66,7 +66,7 @@ default-net = "0.14"
|
||||
wol-rs = "1.0"
|
||||
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
||||
errno = "0.3"
|
||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
||||
rdev = { git = "https://github.com/rustdesk-org/rdev" }
|
||||
url = { version = "2.3", features = ["serde"] }
|
||||
crossbeam-queue = "0.3"
|
||||
hex = "0.4"
|
||||
@@ -89,7 +89,10 @@ sys-locale = "0.3"
|
||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||
clipboard = { path = "libs/clipboard" }
|
||||
ctrlc = "3.2"
|
||||
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
|
||||
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
|
||||
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
|
||||
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||
|
||||
system_shutdown = "4.0"
|
||||
qrcode-generator = "4.1"
|
||||
|
||||
@@ -111,7 +114,7 @@ winapi = { version = "0.3", features = [
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
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"
|
||||
tauri-winrt-notification = "0.1.2"
|
||||
runas = "1.2"
|
||||
@@ -135,7 +138,7 @@ image = "0.24"
|
||||
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
|
||||
wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
@@ -149,21 +152,23 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
||||
async-process = "1.7"
|
||||
mouce = { git="https://github.com/fufesou/mouce.git" }
|
||||
evdev = { git="https://github.com/fufesou/evdev" }
|
||||
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
pam = { git="https://github.com/fufesou/pam" }
|
||||
pam = { git="https://github.com/rustdesk-org/pam" }
|
||||
users = { version = "0.11" }
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
percent-encoding = {version = "2.3", optional = true}
|
||||
once_cell = {version = "1.18", optional = true}
|
||||
nix = { version = "0.29", features = ["term", "process"]}
|
||||
gtk = "0.18"
|
||||
termios = "0.3"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
jni = "0.21"
|
||||
android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
|
||||
android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
|
||||
|
||||
[workspace]
|
||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
||||
|
||||
12
README.md
12
README.md
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
@@ -59,19 +59,19 @@ Please download Sciter dynamic library yourself.
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
@@ -171,3 +171,7 @@ Please ensure that you are running these commands from the root of the RustDesk
|
||||

|
||||
|
||||

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

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

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
|
||||
|
||||

|
||||
|
||||
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md).
|
||||
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ import android.widget.EditText
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.view.KeyEvent as KeyEventAndroid
|
||||
import android.graphics.Rect
|
||||
import android.media.AudioManager
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||
@@ -75,6 +77,8 @@ class InputService : AccessibilityService() {
|
||||
|
||||
private var fakeEditTextForTextStateCalculation: EditText? = null
|
||||
|
||||
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
||||
val x = max(0, _x)
|
||||
@@ -294,6 +298,18 @@ class InputService : AccessibilityService() {
|
||||
|
||||
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
||||
|
||||
var ke: KeyEventAndroid? = null
|
||||
if (Build.VERSION.SDK_INT < 33 || textToCommit == null) {
|
||||
ke = KeyEventConverter.toAndroidKeyEvent(keyEvent)
|
||||
}
|
||||
ke?.let { event ->
|
||||
if (tryHandleVolumeKeyEvent(event)) {
|
||||
return
|
||||
} else if (tryHandlePowerKeyEvent(event)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
getInputMethod()?.let { inputMethod ->
|
||||
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
||||
@@ -302,7 +318,7 @@ class InputService : AccessibilityService() {
|
||||
inputConnection.commitText(text, 1, null)
|
||||
}
|
||||
} else {
|
||||
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event ->
|
||||
ke?.let { event ->
|
||||
inputConnection.sendKeyEvent(event)
|
||||
}
|
||||
}
|
||||
@@ -311,7 +327,7 @@ class InputService : AccessibilityService() {
|
||||
} else {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
handler.post {
|
||||
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event ->
|
||||
ke?.let { event ->
|
||||
val possibleNodes = possibleAccessibiltyNodes()
|
||||
Log.d(logTag, "possibleNodes:$possibleNodes")
|
||||
for (item in possibleNodes) {
|
||||
@@ -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) {
|
||||
if (node == null) {
|
||||
return
|
||||
@@ -422,7 +475,7 @@ class InputService : AccessibilityService() {
|
||||
return linkedList
|
||||
}
|
||||
|
||||
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
||||
private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
||||
node.refresh()
|
||||
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
||||
this.fakeEditTextForTextStateCalculation?.setText(null)
|
||||
@@ -487,10 +540,10 @@ class InputService : AccessibilityService() {
|
||||
|
||||
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
||||
it.onPreDraw()
|
||||
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
|
||||
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||
val succ = it.onKeyDown(event.getKeyCode(), event)
|
||||
Log.d(logTag, "onKeyDown $succ")
|
||||
} else if (event.action == android.view.KeyEvent.ACTION_UP) {
|
||||
} else if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||
val success = it.onKeyUp(event.getKeyCode(), event)
|
||||
Log.d(logTag, "keyup $success")
|
||||
} else {}
|
||||
|
||||
@@ -37,6 +37,8 @@ object KeyEventConverter {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -112,6 +114,10 @@ object KeyEventConverter {
|
||||
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
||||
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
||||
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
||||
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
|
||||
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
|
||||
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
ControlKey.Power -> KeyEvent.KEYCODE_POWER
|
||||
else -> 0 // Default to unknown.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
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!"
|
||||
@@ -31,7 +31,6 @@ import 'mobile/pages/file_manager_page.dart';
|
||||
import 'mobile/pages/remote_page.dart';
|
||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'models/input_model.dart';
|
||||
import 'models/model.dart';
|
||||
import 'models/platform_model.dart';
|
||||
|
||||
@@ -51,6 +50,9 @@ final isLinux = isLinux_;
|
||||
final isDesktop = isDesktop_;
|
||||
final isWeb = isWeb_;
|
||||
final isWebDesktop = isWebDesktop_;
|
||||
final isWebOnWindows = isWebOnWindows_;
|
||||
final isWebOnLinux = isWebOnLinux_;
|
||||
final isWebOnMacOs = isWebOnMacOS_;
|
||||
var isMobile = isAndroid || isIOS;
|
||||
var version = '';
|
||||
int androidVersion = 0;
|
||||
@@ -348,6 +350,9 @@ class MyTheme {
|
||||
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
dialogBackgroundColor: Colors.white,
|
||||
appBarTheme: AppBarTheme(
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
elevation: 15,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -443,6 +448,9 @@ class MyTheme {
|
||||
hoverColor: Color.fromARGB(255, 45, 46, 53),
|
||||
scaffoldBackgroundColor: Color(0xFF18191E),
|
||||
dialogBackgroundColor: Color(0xFF18191E),
|
||||
appBarTheme: AppBarTheme(
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
elevation: 15,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -548,7 +556,7 @@ class MyTheme {
|
||||
|
||||
static void changeDarkMode(ThemeMode mode) async {
|
||||
Get.changeThemeMode(mode);
|
||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
||||
if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
|
||||
if (mode == ThemeMode.system) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
||||
@@ -556,7 +564,7 @@ class MyTheme {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||
}
|
||||
await bind.mainChangeTheme(dark: mode.toShortString());
|
||||
if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
|
||||
// Synchronize the window theme of the system.
|
||||
updateSystemWindowTheme();
|
||||
}
|
||||
@@ -630,10 +638,30 @@ List<Locale> supportedLocales = const [
|
||||
Locale('da'),
|
||||
Locale('eo'),
|
||||
Locale('tr'),
|
||||
Locale('vi'),
|
||||
Locale('pl'),
|
||||
Locale('kz'),
|
||||
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) {
|
||||
@@ -647,8 +675,12 @@ String formatDurationToTime(Duration duration) {
|
||||
|
||||
closeConnection({String? id}) {
|
||||
if (isAndroid || isIOS) {
|
||||
gFFI.chatModel.hideChatOverlay();
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
() async {
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
gFFI.chatModel.hideChatOverlay();
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
}();
|
||||
} else {
|
||||
if (isWeb) {
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
@@ -928,13 +960,9 @@ makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
||||
position: draggablePositions.mobileActions,
|
||||
width: overlayW,
|
||||
height: overlayH,
|
||||
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
||||
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
||||
onRecentPressed: () async {
|
||||
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
||||
},
|
||||
onBackPressed: session.inputModel.onMobileBack,
|
||||
onHomePressed: session.inputModel.onMobileHome,
|
||||
onRecentPressed: session.inputModel.onMobileApps,
|
||||
onHidePressed: onHide,
|
||||
);
|
||||
}
|
||||
@@ -1058,6 +1086,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,
|
||||
String link, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
|
||||
@@ -1066,7 +1137,7 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
bool hasOk = false;
|
||||
submit() {
|
||||
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) {
|
||||
closeConnection();
|
||||
}
|
||||
@@ -1100,21 +1171,33 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
}
|
||||
if (reconnect != null &&
|
||||
title == "Connection Error" &&
|
||||
reconnectTimeout != null) {
|
||||
if (reconnect != null && title == "Connection Error") {
|
||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||
final enabled = true.obs;
|
||||
final button = Obx(() => _ReconnectCountDownButton(
|
||||
second: reconnectTimeout,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
));
|
||||
final button = reconnectTimeout != null
|
||||
? Obx(() => _ReconnectCountDownButton(
|
||||
second: reconnectTimeout,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
))
|
||||
: Obx(
|
||||
() => dialogButton(
|
||||
'Reconnect',
|
||||
isOutline: true,
|
||||
onPressed: enabled.isTrue
|
||||
? () {
|
||||
// Disable the button
|
||||
enabled.value = false;
|
||||
reconnect(dialogManager, sessionId, false);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
buttons.insert(0, button);
|
||||
}
|
||||
if (link.isNotEmpty) {
|
||||
@@ -1203,7 +1286,7 @@ Widget msgboxContent(String type, String title, String text) {
|
||||
translate(title),
|
||||
style: TextStyle(fontSize: 21),
|
||||
).marginOnly(bottom: 10),
|
||||
Text(translateText(text), style: const TextStyle(fontSize: 15)),
|
||||
createDialogContent(translateText(text)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1398,14 +1481,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>(
|
||||
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
|
||||
{EdgeInsetsGeometry? contentPadding, bool? dense}) {
|
||||
{bool? dense}) {
|
||||
return RadioListTile<T>(
|
||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
title: title,
|
||||
@@ -1432,7 +1511,7 @@ Future<void> initGlobalFFI() async {
|
||||
_globalFFI = FFI(null);
|
||||
debugPrint("_globalFFI init end");
|
||||
// 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) {
|
||||
@@ -2719,20 +2798,26 @@ Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
|
||||
}
|
||||
|
||||
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
||||
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
|
||||
var block = false.obs;
|
||||
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,
|
||||
child: Stack(children: [
|
||||
child,
|
||||
Offstage(
|
||||
offstage: !block.value,
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
)),
|
||||
// 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: !block.value,
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
)),
|
||||
]),
|
||||
));
|
||||
}
|
||||
@@ -2800,7 +2885,7 @@ Widget buildErrorBanner(BuildContext context,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Tooltip(
|
||||
message: translate(err.value),
|
||||
child: Text(
|
||||
child: SelectableText(
|
||||
translate(err.value),
|
||||
),
|
||||
)).marginSymmetric(vertical: 2),
|
||||
@@ -2921,6 +3006,16 @@ openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
||||
final displays = i == kAllDisplayValue
|
||||
? List.generate(pi.displays.length, (index) => index)
|
||||
: [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(
|
||||
isDesktop: isDesktop,
|
||||
sessionId: ffi.sessionId,
|
||||
@@ -2954,11 +3049,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||
}
|
||||
|
||||
setNewConnectWindowFrame(
|
||||
int windowId, String peerId, int? display, Rect? screenRect) async {
|
||||
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
|
||||
int? display, Rect? screenRect) async {
|
||||
if (screenRect == null) {
|
||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||
windowId: windowId, display: display, peerId: peerId);
|
||||
// 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,
|
||||
windowId: windowId, display: display, peerId: peerId);
|
||||
}
|
||||
} else {
|
||||
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||
}
|
||||
@@ -3055,6 +3154,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
|
||||
|
||||
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
||||
String? text) {
|
||||
text = text?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
final sc = ServerConfig.decode(text);
|
||||
@@ -3084,9 +3184,16 @@ Future<bool> setServerConfig(
|
||||
List<RxString>? errMsgs,
|
||||
ServerConfig config,
|
||||
) async {
|
||||
config.idServer = config.idServer.trim();
|
||||
config.relayServer = config.relayServer.trim();
|
||||
config.apiServer = config.apiServer.trim();
|
||||
String removeEndSlash(String input) {
|
||||
if (input.endsWith('/')) {
|
||||
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();
|
||||
if (controllers != null) {
|
||||
controllers[0].text = config.idServer;
|
||||
@@ -3295,6 +3402,42 @@ bool isInHomePage() {
|
||||
return controller.state.value.selected == 0;
|
||||
}
|
||||
|
||||
Widget _buildPresetPasswordWarning() {
|
||||
if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) !=
|
||||
'N') {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
color: Colors.yellow,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
child: Text(
|
||||
translate("Security Alert"),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize:
|
||||
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
translate("preset_password_warning"),
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
); // Show a warning message if the Future completed with true
|
||||
}
|
||||
|
||||
Widget buildPresetPasswordWarningMobile() {
|
||||
if (bind.isPresetPasswordMobileOnly()) {
|
||||
return _buildPresetPasswordWarning();
|
||||
} else {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildPresetPasswordWarning() {
|
||||
return FutureBuilder<bool>(
|
||||
future: bind.isPresetPassword(),
|
||||
@@ -3305,27 +3448,7 @@ Widget buildPresetPasswordWarning() {
|
||||
return Text(
|
||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||
} else if (snapshot.hasData && snapshot.data == true) {
|
||||
return Container(
|
||||
color: Colors.yellow,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
child: Text(
|
||||
translate("Security Alert"),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize:
|
||||
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
translate("preset_password_warning"),
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
); // Show a warning message if the Future completed with true
|
||||
return _buildPresetPasswordWarning();
|
||||
} else {
|
||||
return SizedBox
|
||||
.shrink(); // Show nothing if the Future completed with false or null
|
||||
@@ -3379,7 +3502,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.
|
||||
// See _linuxWindowResizable for more details.
|
||||
@@ -3397,7 +3521,12 @@ setResizable(bool resizable) {
|
||||
|
||||
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 defaultOptionTheme => isCustomClient ? 'system' : '';
|
||||
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
||||
@@ -3406,6 +3535,12 @@ get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
||||
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
||||
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.
|
||||
@@ -3429,3 +3564,36 @@ disableWindowMovable(int? windowId) {
|
||||
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) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||
final RxString state = ''.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxString>(state, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxString>(tag: key)) {
|
||||
Get.delete<RxString>(tag: key);
|
||||
} else {
|
||||
Get.find<RxString>(tag: key).value = '';
|
||||
}
|
||||
@@ -33,9 +33,9 @@ class BlockInputState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
@@ -43,8 +43,8 @@ class BlockInputState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ class CurrentDisplayState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||
final RxInt state = RxInt(0);
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxInt>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxInt>(tag: key).value = 0;
|
||||
}
|
||||
@@ -66,8 +66,8 @@ class CurrentDisplayState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||
Get.delete<RxInt>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,16 +105,16 @@ class ConnectionTypeState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<ConnectionType>(tag: key)) {
|
||||
final ConnectionType collectionType = ConnectionType();
|
||||
Get.put(collectionType, tag: key);
|
||||
Get.put<ConnectionType>(collectionType, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<ConnectionType>(tag: key)) {
|
||||
Get.delete<ConnectionType>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,9 +127,9 @@ class FingerprintState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||
final RxString state = ''.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxString>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxString>(tag: key).value = '';
|
||||
}
|
||||
@@ -137,8 +137,8 @@ class FingerprintState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxString>(tag: key)) {
|
||||
Get.delete<RxString>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,9 +150,9 @@ class ShowRemoteCursorState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
@@ -160,8 +160,8 @@ class ShowRemoteCursorState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,9 +173,9 @@ class ShowRemoteCursorLockState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
@@ -183,8 +183,8 @@ class ShowRemoteCursorLockState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,10 +196,10 @@ class KeyboardEnabledState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
// Server side, default true
|
||||
final RxBool state = true.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = true;
|
||||
}
|
||||
@@ -207,8 +207,8 @@ class KeyboardEnabledState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,9 +220,9 @@ class RemoteCursorMovedState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxBool>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = false;
|
||||
}
|
||||
@@ -230,8 +230,8 @@ class RemoteCursorMovedState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +243,9 @@ class RemoteCountState {
|
||||
|
||||
static void init() {
|
||||
final key = tag();
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||
final RxInt state = 1.obs;
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxInt>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxInt>(tag: key).value = 1;
|
||||
}
|
||||
@@ -253,8 +253,8 @@ class RemoteCountState {
|
||||
|
||||
static void delete() {
|
||||
final key = tag();
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||
Get.delete<RxInt>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,9 +266,9 @@ class PeerBoolOption {
|
||||
|
||||
static void init(String id, String opt, bool Function() init_getter) {
|
||||
final key = tag(id, opt);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||
final RxBool value = RxBool(init_getter());
|
||||
Get.put(value, tag: key);
|
||||
Get.put<RxBool>(value, tag: key);
|
||||
} else {
|
||||
Get.find<RxBool>(tag: key).value = init_getter();
|
||||
}
|
||||
@@ -276,8 +276,8 @@ class PeerBoolOption {
|
||||
|
||||
static void delete(String id, String opt) {
|
||||
final key = tag(id, opt);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||
Get.delete<RxBool>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,9 +290,9 @@ class PeerStringOption {
|
||||
|
||||
static void init(String id, String opt, String Function() init_getter) {
|
||||
final key = tag(id, opt);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||
final RxString value = RxString(init_getter());
|
||||
Get.put(value, tag: key);
|
||||
Get.put<RxString>(value, tag: key);
|
||||
} else {
|
||||
Get.find<RxString>(tag: key).value = init_getter();
|
||||
}
|
||||
@@ -300,8 +300,8 @@ class PeerStringOption {
|
||||
|
||||
static void delete(String id, String opt) {
|
||||
final key = tag(id, opt);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxString>(tag: key)) {
|
||||
Get.delete<RxString>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,9 +314,9 @@ class UnreadChatCountState {
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||
final RxInt state = RxInt(0);
|
||||
Get.put(state, tag: key);
|
||||
Get.put<RxInt>(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxInt>(tag: key).value = 0;
|
||||
}
|
||||
@@ -324,8 +324,8 @@ class UnreadChatCountState {
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||
Get.delete<RxInt>(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
import 'package:get/get.dart';
|
||||
@@ -35,17 +36,14 @@ class AddressBook extends StatefulWidget {
|
||||
class _AddressBookState extends State<AddressBook> {
|
||||
var menuPos = RelativeRect.fill;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
if (!gFFI.userModel.isLogin) {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||
return netWorkErrorWidget();
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -64,15 +62,16 @@ class _AddressBookState extends State<AddressBook> {
|
||||
retry: null, // remove retry
|
||||
close: () => gFFI.abModel.currentAbPushError.value = ''),
|
||||
Expanded(
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? _buildAddressBookDesktop()
|
||||
: _buildAddressBookMobile())
|
||||
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? _buildAddressBookPortrait()
|
||||
: _buildAddressBookLandscape()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Widget _buildAddressBookDesktop() {
|
||||
Widget _buildAddressBookLandscape() {
|
||||
return Row(
|
||||
children: [
|
||||
Offstage(
|
||||
@@ -109,7 +108,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddressBookMobile() {
|
||||
Widget _buildAddressBookPortrait() {
|
||||
const padding = 8.0;
|
||||
return Column(
|
||||
children: [
|
||||
@@ -242,14 +241,14 @@ class _AddressBookState extends State<AddressBook> {
|
||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||
}
|
||||
},
|
||||
customButton: Container(
|
||||
height: isDesktop ? 48 : 40,
|
||||
customButton: Obx(()=>Container(
|
||||
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||
Icon(Icons.arrow_drop_down),
|
||||
]),
|
||||
),
|
||||
)),
|
||||
underline: Container(
|
||||
height: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
@@ -338,8 +337,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
showActionMenu: editPermission);
|
||||
}
|
||||
|
||||
final gridView = DynamicGridView.builder(
|
||||
shrinkWrap: isMobile,
|
||||
gridView(bool isPortrait) => DynamicGridView.builder(
|
||||
shrinkWrap: isPortrait,
|
||||
gridDelegate: SliverGridDelegateWithWrapping(),
|
||||
itemCount: tags.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
@@ -347,9 +346,9 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return tagBuilder(e);
|
||||
});
|
||||
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return (isDesktop || isWebDesktop)
|
||||
? gridView
|
||||
: LimitedBox(maxHeight: maxHeight, child: gridView);
|
||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||
? gridView(false)
|
||||
: LimitedBox(maxHeight: maxHeight, child: gridView(true)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -425,7 +424,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
|
||||
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||
sortMenuItem(),
|
||||
if (gFFI.abModel.legacyMode.value)
|
||||
sortMenuItem(), // It's already sorted after pulling down
|
||||
if (canWrite) syncMenuItem(),
|
||||
filterMenuItem(),
|
||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||
@@ -508,9 +508,9 @@ class _AddressBookState extends State<AddressBook> {
|
||||
double marginBottom = 4;
|
||||
|
||||
row({required Widget lable, required Widget input}) {
|
||||
return Row(
|
||||
makeChild(bool isPortrait) => Row(
|
||||
children: [
|
||||
!isMobile
|
||||
!isPortrait
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
@@ -521,7 +521,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isMobile ? 8 : 0);
|
||||
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
@@ -544,24 +545,24 @@ class _AddressBookState extends State<AddressBook> {
|
||||
),
|
||||
],
|
||||
),
|
||||
input: TextField(
|
||||
input: Obx(() => TextField(
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('ID'),
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
)),
|
||||
))),
|
||||
row(
|
||||
lable: Text(
|
||||
translate('Alias'),
|
||||
style: style,
|
||||
),
|
||||
input: TextField(
|
||||
input: Obx(() => TextField(
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('Alias'),
|
||||
)),
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Alias'),
|
||||
),)),
|
||||
),
|
||||
if (isCurrentAbShared)
|
||||
row(
|
||||
@@ -569,11 +570,11 @@ class _AddressBookState extends State<AddressBook> {
|
||||
translate('Password'),
|
||||
style: style,
|
||||
),
|
||||
input: TextField(
|
||||
input: Obx(() => TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('Password'),
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
@@ -587,7 +588,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
},
|
||||
),
|
||||
),
|
||||
)),
|
||||
),)),
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
|
||||
@@ -2,22 +2,39 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
const _kWindowsSystemSound = 'System Sound';
|
||||
|
||||
typedef AudioINputSetDevice = void Function(String device);
|
||||
typedef AudioInputBuilder = Widget Function(
|
||||
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
||||
|
||||
class AudioInput extends StatelessWidget {
|
||||
final AudioInputBuilder builder;
|
||||
final bool isCm;
|
||||
final bool isVoiceCall;
|
||||
|
||||
const AudioInput({Key? key, required this.builder}) : super(key: key);
|
||||
const AudioInput(
|
||||
{Key? key,
|
||||
required this.builder,
|
||||
required this.isCm,
|
||||
required this.isVoiceCall})
|
||||
: super(key: key);
|
||||
|
||||
static String getDefault() {
|
||||
if (isWindows) return translate('System Sound');
|
||||
return '';
|
||||
}
|
||||
|
||||
static Future<String> getValue() async {
|
||||
String device = await bind.mainGetOption(key: 'audio-input');
|
||||
static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
|
||||
if (isVoiceCall) {
|
||||
return bind.getVoiceCallInputDevice(isCm: isCm);
|
||||
} else {
|
||||
return bind.mainGetOption(key: 'audio-input');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
|
||||
String device = await getAudioInput(isCm, isVoiceCall);
|
||||
if (device.isNotEmpty) {
|
||||
return device;
|
||||
} else {
|
||||
@@ -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 = '';
|
||||
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||
if (isVoiceCall) {
|
||||
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
|
||||
} else {
|
||||
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, Object>> getDevicesInfo() async {
|
||||
static Future<Map<String, Object>> getDevicesInfo(
|
||||
bool isCm, bool isVoiceCall) async {
|
||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||
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};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return futureBuilder(
|
||||
future: getDevicesInfo(),
|
||||
future: getDevicesInfo(isCm, isVoiceCall),
|
||||
hasData: (data) {
|
||||
String currentDevice = data['current'];
|
||||
List<String> devices = data['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
return const Offstage();
|
||||
}
|
||||
return builder(devices, currentDevice, setDevice);
|
||||
return builder(devices, currentDevice, (devices) {
|
||||
setDevice(devices, isCm, isVoiceCall);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
message: !(isDesktop || isWebDesktop)
|
||||
? ''
|
||||
: widget.peer.tags.isNotEmpty
|
||||
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
|
||||
|
||||
@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
|
||||
String get name => translate('uppercase');
|
||||
@override
|
||||
bool validate(String value) {
|
||||
return value.contains(RegExp(r'[A-Z]'));
|
||||
return value.runes.any((int rune) {
|
||||
var character = String.fromCharCode(rune);
|
||||
return character.toUpperCase() == character &&
|
||||
character.toLowerCase() != character;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
|
||||
|
||||
@override
|
||||
bool validate(String value) {
|
||||
return value.contains(RegExp(r'[a-z]'));
|
||||
return value.runes.any((int rune) {
|
||||
var character = String.fromCharCode(rune);
|
||||
return character.toLowerCase() == character &&
|
||||
character.toUpperCase() != character;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import 'dart:convert';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
@@ -218,50 +220,53 @@ void changeWhiteList({Function()? callback}) async {
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
if (!isOptFixed)dialogButton("Clear", onPressed: () async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||
callback?.call();
|
||||
close();
|
||||
}, isOutline: true),
|
||||
if (!isOptFixed) dialogButton(
|
||||
"OK",
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
newWhiteListField = controller.text.trim();
|
||||
var newWhiteList = "";
|
||||
if (newWhiteListField.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
// test ip
|
||||
final ipMatch = RegExp(
|
||||
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
||||
final ipv6Match = RegExp(
|
||||
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
|
||||
for (final ip in ips) {
|
||||
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
|
||||
msg = "${translate("Invalid IP")} $ip";
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
newWhiteList = ips.join(',');
|
||||
}
|
||||
if (newWhiteList.trim().isEmpty) {
|
||||
newWhiteList = defaultOptionWhitelist;
|
||||
}
|
||||
if (!isOptFixed)
|
||||
dialogButton("Clear", onPressed: () async {
|
||||
await bind.mainSetOption(
|
||||
key: kOptionWhitelist, value: newWhiteList);
|
||||
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||
callback?.call();
|
||||
close();
|
||||
},
|
||||
),
|
||||
}, isOutline: true),
|
||||
if (!isOptFixed)
|
||||
dialogButton(
|
||||
"OK",
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
newWhiteListField = controller.text.trim();
|
||||
var newWhiteList = "";
|
||||
if (newWhiteListField.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ips =
|
||||
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
// test ip
|
||||
final ipMatch = RegExp(
|
||||
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
||||
final ipv6Match = RegExp(
|
||||
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
|
||||
for (final ip in ips) {
|
||||
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
|
||||
msg = "${translate("Invalid IP")} $ip";
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
newWhiteList = ips.join(',');
|
||||
}
|
||||
if (newWhiteList.trim().isEmpty) {
|
||||
newWhiteList = defaultOptionWhitelist;
|
||||
}
|
||||
await bind.mainSetOption(
|
||||
key: kOptionWhitelist, value: newWhiteList);
|
||||
callback?.call();
|
||||
close();
|
||||
},
|
||||
),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
@@ -376,6 +381,7 @@ class DialogTextField extends StatelessWidget {
|
||||
final FocusNode? focusNode;
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final int? maxLength;
|
||||
|
||||
static const kUsernameTitle = 'Username';
|
||||
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
||||
@@ -393,6 +399,7 @@ class DialogTextField extends StatelessWidget {
|
||||
this.hintText,
|
||||
this.keyboardType,
|
||||
this.inputFormatters,
|
||||
this.maxLength,
|
||||
required this.title,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
@@ -419,6 +426,7 @@ class DialogTextField extends StatelessWidget {
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -675,6 +683,8 @@ class PasswordWidget extends StatefulWidget {
|
||||
this.reRequestFocus = false,
|
||||
this.hintText,
|
||||
this.errorText,
|
||||
this.title,
|
||||
this.maxLength,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
@@ -682,6 +692,8 @@ class PasswordWidget extends StatefulWidget {
|
||||
final bool reRequestFocus;
|
||||
final String? hintText;
|
||||
final String? errorText;
|
||||
final String? title;
|
||||
final int? maxLength;
|
||||
|
||||
@override
|
||||
State<PasswordWidget> createState() => _PasswordWidgetState();
|
||||
@@ -725,7 +737,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DialogTextField(
|
||||
title: translate(DialogTextField.kPasswordTitle),
|
||||
title: translate(widget.title ?? DialogTextField.kPasswordTitle),
|
||||
hintText: translate(widget.hintText ?? 'Enter your password'),
|
||||
controller: widget.controller,
|
||||
prefixIcon: DialogTextField.kPasswordIcon,
|
||||
@@ -744,6 +756,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
obscureText: !_passwordVisible,
|
||||
errorText: widget.errorText,
|
||||
focusNode: _focusNode,
|
||||
maxLength: widget.maxLength,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1117,7 +1130,7 @@ void showRequestElevationDialog(
|
||||
errorText: errPwd.isEmpty ? null : errPwd.value,
|
||||
),
|
||||
],
|
||||
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
|
||||
).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
|
||||
).marginOnly(top: 10),
|
||||
],
|
||||
),
|
||||
@@ -1762,9 +1775,70 @@ void renameDialog(
|
||||
});
|
||||
}
|
||||
|
||||
void changeBot({Function()? callback}) async {
|
||||
if (bind.mainHasValidBotSync()) {
|
||||
await bind.mainSetOption(key: "bot", value: "");
|
||||
callback?.call();
|
||||
return;
|
||||
}
|
||||
String errorText = '';
|
||||
bool loading = false;
|
||||
final controller = TextEditingController();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
onVerify() async {
|
||||
final token = controller.text.trim();
|
||||
if (token == "") return;
|
||||
loading = true;
|
||||
errorText = '';
|
||||
setState(() {});
|
||||
final error = await bind.mainVerifyBot(token: token);
|
||||
if (error == "") {
|
||||
callback?.call();
|
||||
close();
|
||||
} else {
|
||||
errorText = translate(error);
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
final codeField = TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: translate('Token'),
|
||||
),
|
||||
);
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Telegram bot")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(translate("enable-bot-desc"),
|
||||
style: TextStyle(fontSize: 12))
|
||||
.marginOnly(bottom: 12),
|
||||
Row(children: [Expanded(child: codeField)]),
|
||||
if (errorText != '')
|
||||
Text(errorText, style: TextStyle(color: Colors.red))
|
||||
.marginOnly(top: 12),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
loading
|
||||
? CircularProgressIndicator()
|
||||
: dialogButton("OK", onPressed: onVerify),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void change2fa({Function()? callback}) async {
|
||||
if (bind.mainHasValid2FaSync()) {
|
||||
await bind.mainSetOption(key: "2fa", value: "");
|
||||
await bind.mainClearTrustedDevices();
|
||||
callback?.call();
|
||||
return;
|
||||
}
|
||||
@@ -1832,6 +1906,7 @@ void enter2FaDialog(
|
||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
final RxBool submitReady = false.obs;
|
||||
final RxBool trustThisDevice = false.obs;
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close, context) {
|
||||
@@ -1841,7 +1916,7 @@ void enter2FaDialog(
|
||||
}
|
||||
|
||||
submit() {
|
||||
gFFI.send2FA(sessionId, controller.text.trim());
|
||||
gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
@@ -1855,9 +1930,27 @@ void enter2FaDialog(
|
||||
onChanged: () => submitReady.value = codeField.isReady,
|
||||
);
|
||||
|
||||
final trustField = Obx(() => CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(translate("Trust this device")),
|
||||
value: trustThisDevice.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
trustThisDevice.value = value;
|
||||
},
|
||||
));
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('enter-2fa-title')),
|
||||
content: codeField,
|
||||
content: Column(
|
||||
children: [
|
||||
codeField,
|
||||
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
|
||||
trustField,
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton('Cancel',
|
||||
onPressed: cancel,
|
||||
@@ -2124,3 +2217,283 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
|
||||
VoidCallback onConfirm) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
close();
|
||||
onConfirm.call();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(content,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
textAlign: TextAlign.start),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 12),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void changeUnlockPinDialog(String oldPin, Function() callback) {
|
||||
final pinController = TextEditingController(text: oldPin);
|
||||
final confirmController = TextEditingController(text: oldPin);
|
||||
String? pinErrorText;
|
||||
String? confirmationErrorText;
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
pinErrorText = null;
|
||||
confirmationErrorText = null;
|
||||
final pin = pinController.text.trim();
|
||||
final confirm = confirmController.text.trim();
|
||||
if (pin != confirm) {
|
||||
setState(() {
|
||||
confirmationErrorText =
|
||||
translate('The confirmation is not identical.');
|
||||
});
|
||||
return;
|
||||
}
|
||||
final errorMsg = bind.mainSetUnlockPin(pin: pin);
|
||||
if (errorMsg != '') {
|
||||
setState(() {
|
||||
pinErrorText = translate(errorMsg);
|
||||
});
|
||||
return;
|
||||
}
|
||||
callback.call();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set PIN")),
|
||||
content: Column(
|
||||
children: [
|
||||
DialogTextField(
|
||||
title: 'PIN',
|
||||
controller: pinController,
|
||||
obscureText: true,
|
||||
errorText: pinErrorText,
|
||||
maxLength: maxLength,
|
||||
),
|
||||
DialogTextField(
|
||||
title: translate('Confirmation'),
|
||||
controller: confirmController,
|
||||
obscureText: true,
|
||||
errorText: confirmationErrorText,
|
||||
maxLength: maxLength,
|
||||
)
|
||||
],
|
||||
).marginOnly(bottom: 12),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void checkUnlockPinDialog(String correctPin, Function() passCallback) {
|
||||
final controller = TextEditingController();
|
||||
String? errorText;
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
final pin = controller.text.trim();
|
||||
if (correctPin != pin) {
|
||||
setState(() {
|
||||
errorText = translate('Wrong PIN');
|
||||
});
|
||||
return;
|
||||
}
|
||||
passCallback.call();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PasswordWidget(
|
||||
title: 'PIN',
|
||||
controller: controller,
|
||||
errorText: errorText,
|
||||
hintText: '',
|
||||
))
|
||||
],
|
||||
).marginOnly(bottom: 12),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void confrimDeleteTrustedDevicesDialog(
|
||||
RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
|
||||
CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
|
||||
() async {
|
||||
if (selectedDevices.isEmpty) return;
|
||||
if (selectedDevices.length == trustedDevices.length) {
|
||||
await bind.mainClearTrustedDevices();
|
||||
trustedDevices.clear();
|
||||
selectedDevices.clear();
|
||||
} else {
|
||||
final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
|
||||
await bind.mainRemoveTrustedDevices(json: json);
|
||||
trustedDevices.removeWhere((element) {
|
||||
return selectedDevices.contains(element.hwid);
|
||||
});
|
||||
selectedDevices.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void manageTrustedDeviceDialog() async {
|
||||
RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
|
||||
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Manage trusted devices")),
|
||||
content: trustedDevicesTable(trustedDevices, selectedDevices),
|
||||
actions: [
|
||||
Obx(() => dialogButton(translate("Delete"),
|
||||
onPressed: selectedDevices.isEmpty
|
||||
? null
|
||||
: () {
|
||||
confrimDeleteTrustedDevicesDialog(
|
||||
trustedDevices,
|
||||
selectedDevices,
|
||||
);
|
||||
},
|
||||
isOutline: false)
|
||||
.marginOnly(top: 12)),
|
||||
dialogButton(translate("Close"), onPressed: close, isOutline: true)
|
||||
.marginOnly(top: 12),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class TrustedDevice {
|
||||
late final Uint8List hwid;
|
||||
late final int time;
|
||||
late final String id;
|
||||
late final String name;
|
||||
late final String platform;
|
||||
|
||||
TrustedDevice.fromJson(Map<String, dynamic> json) {
|
||||
final hwidList = json['hwid'] as List<dynamic>;
|
||||
hwid = Uint8List.fromList(hwidList.cast<int>());
|
||||
time = json['time'];
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
platform = json['platform'];
|
||||
}
|
||||
|
||||
String daysRemaining() {
|
||||
final expiry = time + 90 * 24 * 60 * 60 * 1000;
|
||||
final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
|
||||
if (remaining < 0) {
|
||||
return '0';
|
||||
}
|
||||
return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
|
||||
}
|
||||
|
||||
static Future<List<TrustedDevice>> get() async {
|
||||
final List<TrustedDevice> devices = List.empty(growable: true);
|
||||
try {
|
||||
final devicesJson = await bind.mainGetTrustedDevices();
|
||||
if (devicesJson.isNotEmpty) {
|
||||
final devicesList = json.decode(devicesJson);
|
||||
if (devicesList is List) {
|
||||
for (var device in devicesList) {
|
||||
devices.add(TrustedDevice.fromJson(device));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
devices.sort((a, b) => b.time.compareTo(a.time));
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
|
||||
Widget trustedDevicesTable(
|
||||
RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
|
||||
RxBool selectAll = false.obs;
|
||||
setSelectAll() {
|
||||
if (selectedDevices.isNotEmpty &&
|
||||
selectedDevices.length == devices.length) {
|
||||
selectAll.value = true;
|
||||
} else {
|
||||
selectAll.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
devices.listen((_) {
|
||||
setSelectAll();
|
||||
});
|
||||
selectedDevices.listen((_) {
|
||||
setSelectAll();
|
||||
});
|
||||
return FittedBox(
|
||||
child: Obx(() => DataTable(
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Checkbox(
|
||||
value: selectAll.value,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
selectedDevices.clear();
|
||||
selectedDevices.addAll(devices.map((e) => e.hwid));
|
||||
} else {
|
||||
selectedDevices.clear();
|
||||
}
|
||||
},
|
||||
)),
|
||||
DataColumn(label: Text(translate('Platform'))),
|
||||
DataColumn(label: Text(translate('ID'))),
|
||||
DataColumn(label: Text(translate('Username'))),
|
||||
DataColumn(label: Text(translate('Days remaining'))),
|
||||
],
|
||||
rows: devices.map((device) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Checkbox(
|
||||
value: selectedDevices.contains(device.hwid),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
selectedDevices.remove(device.hwid);
|
||||
selectedDevices.add(device.hwid);
|
||||
} else {
|
||||
selectedDevices.remove(device.hwid);
|
||||
}
|
||||
},
|
||||
)),
|
||||
DataCell(Text(device.platform)),
|
||||
DataCell(Text(device.id)),
|
||||
DataCell(Text(device.name)),
|
||||
DataCell(Text(device.daysRemaining())),
|
||||
]);
|
||||
}).toList(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
|
||||
String _failedMsg = '';
|
||||
String _url = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/common/widgets/login.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
@@ -23,11 +24,6 @@ class _MyGroupState extends State<MyGroup> {
|
||||
RxString get searchUserText => gFFI.groupModel.searchUserText;
|
||||
static TextEditingController searchUserController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
@@ -35,6 +31,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||
return netWorkErrorWidget();
|
||||
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
@@ -48,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
|
||||
retry: null,
|
||||
close: () => gFFI.groupModel.groupLoadError.value = ''),
|
||||
Expanded(
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? _buildDesktop()
|
||||
: _buildMobile())
|
||||
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? _buildPortrait()
|
||||
: _buildLandscape())),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDesktop() {
|
||||
Widget _buildLandscape() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
@@ -92,7 +90,7 @@ class _MyGroupState extends State<MyGroup> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobile() {
|
||||
Widget _buildPortrait() {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -162,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
final listView = ListView.builder(
|
||||
shrinkWrap: isMobile,
|
||||
listView(bool isPortrait) => ListView.builder(
|
||||
shrinkWrap: isPortrait,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return (isDesktop || isWebDesktop)
|
||||
? listView
|
||||
: LimitedBox(maxHeight: maxHeight, child: listView);
|
||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||
? listView(false)
|
||||
: LimitedBox(maxHeight: maxHeight, child: listView(true)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ class Draggable extends StatefulWidget {
|
||||
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DraggableState();
|
||||
State<StatefulWidget> createState() => _DraggableState(chatModel);
|
||||
}
|
||||
|
||||
class _DraggableState extends State<Draggable> {
|
||||
@@ -362,10 +362,8 @@ class _DraggableState extends State<Draggable> {
|
||||
double _saveHeight = 0;
|
||||
double _lastBottomHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_chatModel = widget.chatModel;
|
||||
_DraggableState(ChatModel? chatModel) {
|
||||
_chatModel = chatModel;
|
||||
}
|
||||
|
||||
get position => widget.position.pos;
|
||||
@@ -467,7 +465,8 @@ class IOSDraggable extends StatefulWidget {
|
||||
final Widget Function(BuildContext) builder;
|
||||
|
||||
@override
|
||||
IOSDraggableState createState() => IOSDraggableState();
|
||||
IOSDraggableState createState() =>
|
||||
IOSDraggableState(chatModel, width, height);
|
||||
}
|
||||
|
||||
class IOSDraggableState extends State<IOSDraggable> {
|
||||
@@ -478,15 +477,13 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
double _saveHeight = 0;
|
||||
double _lastBottomHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_chatModel = widget.chatModel;
|
||||
_width = widget.width;
|
||||
_height = widget.height;
|
||||
IOSDraggableState(ChatModel? chatModel, double w, double h) {
|
||||
_chatModel = chatModel;
|
||||
_width = w;
|
||||
_height = h;
|
||||
}
|
||||
|
||||
get position => widget.position;
|
||||
DraggableKeyPosition get position => widget.position;
|
||||
|
||||
checkKeyboard() {
|
||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
@@ -494,13 +491,13 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
|
||||
// save
|
||||
if (!_keyboardVisible && currentVisible) {
|
||||
_saveHeight = position.value.dy;
|
||||
_saveHeight = position.pos.dy;
|
||||
}
|
||||
|
||||
// reset
|
||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||
setState(() {
|
||||
position.value = Offset(position.value.dx, _saveHeight);
|
||||
position.update(Offset(position.pos.dx, _saveHeight));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -508,10 +505,10 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
if (_keyboardVisible && currentVisible) {
|
||||
final sumHeight = bottomHeight + _height;
|
||||
final contextHeight = MediaQuery.of(context).size.height;
|
||||
if (sumHeight + position.value.dy > contextHeight) {
|
||||
if (sumHeight + position.pos.dy > contextHeight) {
|
||||
final y = contextHeight - sumHeight;
|
||||
setState(() {
|
||||
position.value = Offset(position.value.dx, y);
|
||||
position.update(Offset(position.pos.dx, y));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -526,14 +523,14 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: position.value.dx,
|
||||
top: position.value.dy,
|
||||
left: position.pos.dx,
|
||||
top: position.pos.dy,
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
setState(() {
|
||||
position.value += details.delta;
|
||||
position.update(position.pos + details.delta);
|
||||
});
|
||||
_chatModel?.setChatWindowPosition(position.value);
|
||||
_chatModel?.setChatWindowPosition(position.pos);
|
||||
},
|
||||
child: Material(
|
||||
child: Container(
|
||||
@@ -598,7 +595,8 @@ class QualityMonitor extends StatelessWidget {
|
||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||
_row(
|
||||
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
if (!isWeb)
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -22,6 +23,8 @@ enum PeerUiType { grid, tile, list }
|
||||
|
||||
final peerCardUiType = PeerUiType.grid.obs;
|
||||
|
||||
bool? hideUsernameOnCard;
|
||||
|
||||
class _PeerCard extends StatefulWidget {
|
||||
final Peer peer;
|
||||
final PeerTabIndex tab;
|
||||
@@ -51,14 +54,11 @@ class _PeerCardState extends State<_PeerCard>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
if (isDesktop || isWebDesktop) {
|
||||
return _buildDesktop();
|
||||
} else {
|
||||
return _buildMobile();
|
||||
}
|
||||
return Obx(() =>
|
||||
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
|
||||
}
|
||||
|
||||
Widget _buildMobile() {
|
||||
Widget _buildPortrait() {
|
||||
final peer = super.widget.peer;
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
return Card(
|
||||
@@ -85,7 +85,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildDesktop() {
|
||||
Widget _buildLandscape() {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final peer = super.widget.peer;
|
||||
var deco = Rx<BoxDecoration?>(
|
||||
@@ -130,18 +130,21 @@ class _PeerCardState extends State<_PeerCard>
|
||||
|
||||
Widget _buildPeerTile(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||
final name =
|
||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
hideUsernameOnCard ??=
|
||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||
final name = hideUsernameOnCard == true
|
||||
? peer.hostname
|
||||
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
final greyStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||
final child = Row(
|
||||
makeChild(bool isPortrait) => Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||
borderRadius: isMobile
|
||||
borderRadius: isPortrait
|
||||
? BorderRadius.circular(_tileRadius)
|
||||
: BorderRadius.only(
|
||||
topLeft: Radius.circular(_tileRadius),
|
||||
@@ -149,11 +152,11 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
width: isMobile ? 50 : 42,
|
||||
height: isMobile ? 50 : null,
|
||||
width: isPortrait ? 50 : 42,
|
||||
height: isPortrait ? 50 : null,
|
||||
child: Stack(
|
||||
children: [
|
||||
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
||||
getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
|
||||
.paddingAll(6),
|
||||
if (_shouldBuildPasswordIcon(peer))
|
||||
Positioned(
|
||||
@@ -178,19 +181,19 @@ class _PeerCardState extends State<_PeerCard>
|
||||
child: Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
getOnline(isMobile ? 4 : 8, peer.online),
|
||||
getOnline(isPortrait ? 4 : 8, peer.online),
|
||||
Expanded(
|
||||
child: Text(
|
||||
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
)),
|
||||
]).marginOnly(top: isMobile ? 0 : 2),
|
||||
]).marginOnly(top: isPortrait ? 0 : 2),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: isMobile ? null : greyStyle,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -198,9 +201,9 @@ class _PeerCardState extends State<_PeerCard>
|
||||
],
|
||||
).marginOnly(top: 2),
|
||||
),
|
||||
isMobile
|
||||
? checkBoxOrActionMoreMobile(peer)
|
||||
: checkBoxOrActionMoreDesktop(peer, isTile: true),
|
||||
isPortrait
|
||||
? checkBoxOrActionMorePortrait(peer)
|
||||
: checkBoxOrActionMoreLandscape(peer, isTile: true),
|
||||
],
|
||||
).paddingOnly(left: 10.0, top: 3.0),
|
||||
),
|
||||
@@ -211,40 +214,45 @@ class _PeerCardState extends State<_PeerCard>
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
message: !(isDesktop || isWebDesktop)
|
||||
? ''
|
||||
: peer.tags.isNotEmpty
|
||||
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
||||
: '',
|
||||
child: Stack(children: [
|
||||
deco == null
|
||||
? child
|
||||
: Obx(
|
||||
() => Container(
|
||||
Obx(() => deco == null
|
||||
? makeChild(stateGlobal.isPortrait.isTrue)
|
||||
: Container(
|
||||
foregroundDecoration: deco.value,
|
||||
child: child,
|
||||
child: makeChild(stateGlobal.isPortrait.isTrue),
|
||||
),
|
||||
),
|
||||
if (colors.isNotEmpty)
|
||||
Positioned(
|
||||
Obx(()=> Positioned(
|
||||
top: 2,
|
||||
right: isMobile ? 20 : 10,
|
||||
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
)
|
||||
))
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeerCard(
|
||||
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
||||
final name =
|
||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
hideUsernameOnCard ??=
|
||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||
final name = hideUsernameOnCard == true
|
||||
? peer.hostname
|
||||
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||
final child = Card(
|
||||
color: Colors.transparent,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
// to-do: memory leak here, more investigation needed.
|
||||
// Continious rebuilds of `Obx()` will cause memory leak here.
|
||||
// The simple demo does not have this issue.
|
||||
child: Obx(
|
||||
() => Container(
|
||||
foregroundDecoration: deco.value,
|
||||
@@ -308,7 +316,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
)),
|
||||
]).paddingSymmetric(vertical: 8)),
|
||||
checkBoxOrActionMoreDesktop(peer, isTile: false),
|
||||
checkBoxOrActionMoreLandscape(peer, isTile: false),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12.0),
|
||||
)
|
||||
@@ -354,7 +362,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
}
|
||||
}
|
||||
|
||||
Widget checkBoxOrActionMoreMobile(Peer peer) {
|
||||
Widget checkBoxOrActionMorePortrait(Peer peer) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
@@ -382,7 +390,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
}
|
||||
}
|
||||
|
||||
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) {
|
||||
Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
@@ -628,8 +636,8 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
|
||||
@protected
|
||||
Future<bool> _isForceAlwaysRelay(String id) async {
|
||||
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))
|
||||
.isNotEmpty;
|
||||
return option2bool(kOptionForceAlwaysRelay,
|
||||
(await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -1195,6 +1203,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
}
|
||||
|
||||
void _rdpDialog(String id) async {
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
|
||||
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
|
||||
final portController = TextEditingController(text: port);
|
||||
@@ -1249,9 +1258,9 @@ void _rdpDialog(String id) async {
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
Row(
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
(isDesktop || isWebDesktop)
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
@@ -1262,17 +1271,17 @@ void _rdpDialog(String id) async {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: (isDesktop || isWebDesktop)
|
||||
labelText: isDesktop
|
||||
? null
|
||||
: translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
|
||||
Row(
|
||||
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
(isDesktop || isWebDesktop)
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
@@ -1283,8 +1292,9 @@ void _rdpDialog(String id) async {
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
maxLength: maxLength,
|
||||
decoration: InputDecoration(
|
||||
labelText: (isDesktop || isWebDesktop)
|
||||
labelText: isDesktop
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
@@ -1296,7 +1306,7 @@ void _rdpDialog(String id) async {
|
||||
)),
|
||||
),
|
||||
],
|
||||
)
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -76,8 +77,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
|
||||
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_PeerTabPageState() {
|
||||
_loadLocalOptions();
|
||||
}
|
||||
|
||||
void _loadLocalOptions() {
|
||||
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||
if (uiType != '') {
|
||||
peerCardUiType.value = int.parse(uiType) == 0
|
||||
@@ -87,8 +91,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
: PeerUiType.list;
|
||||
}
|
||||
hideAbTagsPanel.value =
|
||||
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
|
||||
super.initState();
|
||||
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
|
||||
}
|
||||
|
||||
Future<void> handleTabSelection(int tabIndex) async {
|
||||
@@ -105,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Widget build(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget selectionWrap(Widget widget) {
|
||||
return model.multiSelectionMode ? createMultiSelectionBar() : widget;
|
||||
return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
|
||||
}
|
||||
|
||||
return Column(
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Container(
|
||||
padding: (isDesktop || isWebDesktop)
|
||||
? null
|
||||
: EdgeInsets.symmetric(horizontal: 2),
|
||||
child: selectionWrap(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
visibleContextMenuListener(_createSwitchBar(context))),
|
||||
if (isMobile)
|
||||
..._mobileRightActions(context)
|
||||
else
|
||||
..._desktopRightActions(context)
|
||||
],
|
||||
)),
|
||||
),
|
||||
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
|
||||
Obx(() => SizedBox(
|
||||
height: 32,
|
||||
child: Container(
|
||||
padding: stateGlobal.isPortrait.isTrue
|
||||
? EdgeInsets.symmetric(horizontal: 2)
|
||||
: null,
|
||||
child: selectionWrap(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: visibleContextMenuListener(
|
||||
_createSwitchBar(context))),
|
||||
if (stateGlobal.isPortrait.isTrue)
|
||||
..._portraitRightActions(context)
|
||||
else
|
||||
..._landscapeRightActions(context)
|
||||
],
|
||||
)),
|
||||
),
|
||||
).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
|
||||
_createPeersView(),
|
||||
],
|
||||
);
|
||||
@@ -297,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
}
|
||||
|
||||
Widget visibleContextMenuListener(Widget child) {
|
||||
if (isMobile) {
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
return GestureDetector(
|
||||
onLongPressDown: (e) {
|
||||
final x = e.globalPosition.dx;
|
||||
@@ -359,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
.toList());
|
||||
}
|
||||
|
||||
Widget createMultiSelectionBar() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget createMultiSelectionBar(PeerTabModel model) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -378,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Row(
|
||||
children: [
|
||||
selectionCount(model.selectedPeers.length),
|
||||
selectAll(),
|
||||
selectAll(model),
|
||||
closeSelection(),
|
||||
],
|
||||
)
|
||||
@@ -399,9 +401,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
final peers = model.selectedPeers;
|
||||
switch (model.currentTab) {
|
||||
case 0:
|
||||
peers.map((p) async {
|
||||
for (var p in peers) {
|
||||
await bind.mainRemovePeer(id: p.id);
|
||||
}).toList();
|
||||
}
|
||||
await bind.mainLoadRecentPeers();
|
||||
break;
|
||||
case 1:
|
||||
@@ -413,9 +415,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
await bind.mainLoadFavPeers();
|
||||
break;
|
||||
case 2:
|
||||
peers.map((p) async {
|
||||
for (var p in peers) {
|
||||
await bind.mainRemoveDiscovered(id: p.id);
|
||||
}).toList();
|
||||
}
|
||||
await bind.mainLoadLanPeers();
|
||||
break;
|
||||
case 3:
|
||||
@@ -454,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
showToast(translate('Successful'));
|
||||
},
|
||||
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -475,7 +477,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
model.setMultiSelectionMode(false);
|
||||
},
|
||||
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -498,7 +500,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
});
|
||||
},
|
||||
child: Icon(Icons.tag))
|
||||
.marginOnly(left: isMobile ? 11 : 6),
|
||||
.marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -509,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget selectAll() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget selectAll(PeerTabModel model) {
|
||||
return Offstage(
|
||||
offstage:
|
||||
model.selectedPeers.length >= model.currentTabCachedPeers.length,
|
||||
@@ -554,10 +555,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _desktopRightActions(BuildContext context) {
|
||||
List<Widget> _landscapeRightActions(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
return [
|
||||
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
|
||||
const PeerSearchBar().marginOnly(right: 13),
|
||||
_createRefresh(
|
||||
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
||||
_createRefresh(
|
||||
@@ -578,7 +579,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _mobileRightActions(BuildContext context) {
|
||||
List<Widget> _portraitRightActions(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
|
||||
@@ -699,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
baseOffset: 0,
|
||||
extentOffset: peerSearchTextController.value.text.length);
|
||||
});
|
||||
return Container(
|
||||
width: isMobile ? 120 : 140,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Obx(() => Row(
|
||||
return Obx(() => Container(
|
||||
width: stateGlobal.isPortrait.isTrue ? 120 : 140,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
@@ -766,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
);
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,16 +873,18 @@ class PeerSortDropdown extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
@override
|
||||
void initState() {
|
||||
_PeerSortDropdownState() {
|
||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||
peerSort.value = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: kOptionPeerSorting,
|
||||
v: peerSort.value,
|
||||
);
|
||||
_loadLocalOptions();
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _loadLocalOptions() {
|
||||
peerSort.value = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: kOptionPeerSorting,
|
||||
v: peerSort.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
@@ -45,10 +46,14 @@ class LoadEvent {
|
||||
final peerSearchText = "".obs;
|
||||
|
||||
/// 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
|
||||
final obslist = [peerSearchText, peerSort].obs;
|
||||
RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
|
||||
|
||||
final peerSearchTextController =
|
||||
TextEditingController(text: peerSearchText.value);
|
||||
@@ -70,7 +75,8 @@ class _PeersView extends StatefulWidget {
|
||||
}
|
||||
|
||||
/// State for the peer widget.
|
||||
class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
class _PeersViewState extends State<_PeersView>
|
||||
with WindowListener, WidgetsBindingObserver {
|
||||
static const int _maxQueryCount = 3;
|
||||
final HashMap<String, String> _emptyMessages = HashMap.from({
|
||||
LoadEvent.recent: 'empty_recent_tip',
|
||||
@@ -82,9 +88,11 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
final _curPeers = <String>{};
|
||||
var _lastChangeTime = DateTime.now();
|
||||
var _lastQueryPeers = <String>{};
|
||||
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
|
||||
var _lastQueryTime = DateTime.now();
|
||||
var _lastWindowRestoreTime = DateTime.now();
|
||||
var _queryCount = 0;
|
||||
var _exit = false;
|
||||
bool _isActive = true;
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@@ -95,12 +103,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
windowManager.removeListener(this);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_exit = true;
|
||||
super.dispose();
|
||||
}
|
||||
@@ -108,15 +118,58 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
_queryCount = 0;
|
||||
_isActive = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
// We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
|
||||
// Maybe it's a bug of the window manager, but the source code seems to be correct.
|
||||
//
|
||||
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
|
||||
// we need the following comparison to ensure that `_isActive` is true in the end.
|
||||
if (isWindows && DateTime.now().difference(_lastWindowRestoreTime) <
|
||||
const Duration(milliseconds: 300)) {
|
||||
return;
|
||||
}
|
||||
_queryCount = _maxQueryCount;
|
||||
_isActive = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowRestore() {
|
||||
// Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
|
||||
// But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
|
||||
if (!isWindows) return;
|
||||
_queryCount = 0;
|
||||
_isActive = true;
|
||||
_lastWindowRestoreTime = DateTime.now();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
_queryCount = _maxQueryCount;
|
||||
// Window minimize also triggers `onWindowBlur()`.
|
||||
}
|
||||
|
||||
// This function is required for mobile.
|
||||
// `onWindowFocus` works fine for desktop.
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (isDesktop || isWebDesktop) return;
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_isActive = true;
|
||||
_queryCount = 0;
|
||||
} else if (state == AppLifecycleState.inactive) {
|
||||
_isActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
|
||||
// Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
|
||||
// Simple demo can reproduce this issue.
|
||||
return ChangeNotifierProvider<Peers>(
|
||||
create: (context) => widget.peers,
|
||||
child: Consumer<Peers>(builder: (context, peers, child) {
|
||||
@@ -172,7 +225,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
var peers = snapshot.data!;
|
||||
if (peers.length > 1000) peers = peers.sublist(0, 1000);
|
||||
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
|
||||
buildOnePeer(Peer peer) {
|
||||
buildOnePeer(Peer peer, bool isPortrait) {
|
||||
final visibilityChild = VisibilityDetector(
|
||||
key: ValueKey(_cardId(peer.id)),
|
||||
onVisibilityChanged: onVisibilityChanged,
|
||||
@@ -184,7 +237,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
// No need to listen the currentTab change event.
|
||||
// Because the currentTab change event will trigger the peers change event,
|
||||
// and the peers change event will trigger _buildPeersView().
|
||||
return (isDesktop || isWebDesktop)
|
||||
return !isPortrait
|
||||
? Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? Container(height: 45, child: visibilityChild)
|
||||
: peerCardUiType.value == PeerUiType.grid
|
||||
@@ -195,44 +248,45 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
: Container(child: visibilityChild);
|
||||
}
|
||||
|
||||
final Widget child;
|
||||
if (isMobile) {
|
||||
child = ListView.builder(
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]).marginOnly(
|
||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child = Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]).marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
}),
|
||||
)
|
||||
: DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
}),
|
||||
));
|
||||
}
|
||||
// We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
|
||||
// Continious rebuilds of `ListView.builder` will cause memory leak.
|
||||
// Simple demo can reproduce this issue.
|
||||
final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? ListView.builder(
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], true).marginOnly(
|
||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||
},
|
||||
)
|
||||
: peerCardUiType.value == PeerUiType.list
|
||||
? DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false)
|
||||
.marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
}),
|
||||
)
|
||||
: DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false);
|
||||
}),
|
||||
));
|
||||
|
||||
if (updateEvent == UpdateEvent.load) {
|
||||
_curPeers.clear();
|
||||
@@ -253,10 +307,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
return body;
|
||||
}
|
||||
|
||||
final _queryInterval = const Duration(seconds: 20);
|
||||
var _queryInterval = const Duration(seconds: 20);
|
||||
|
||||
void _startCheckOnlines() {
|
||||
() async {
|
||||
final p = await bind.mainIsUsingPublicServer();
|
||||
if (!p) {
|
||||
_queryInterval = const Duration(seconds: 6);
|
||||
}
|
||||
while (!_exit) {
|
||||
final now = DateTime.now();
|
||||
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
||||
@@ -264,7 +322,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
_queryOnlines(false);
|
||||
}
|
||||
} else {
|
||||
if (_queryCount < _maxQueryCount) {
|
||||
if (_isActive && (_queryCount < _maxQueryCount || !p)) {
|
||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
@@ -282,14 +340,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
_queryOnlines(bool isLoadEvent) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
_lastQueryPeers = {..._curPeers};
|
||||
if (isLoadEvent) {
|
||||
_lastChangeTime = DateTime.now();
|
||||
} else {
|
||||
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
||||
}
|
||||
_queryCount = 0;
|
||||
}
|
||||
_lastQueryPeers = {..._curPeers};
|
||||
if (isLoadEvent) {
|
||||
_lastChangeTime = DateTime.now();
|
||||
} else {
|
||||
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Peer>>? matchPeers(
|
||||
|
||||
@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// https://github.com/flutter/flutter/issues/154053
|
||||
final useRawKeyEvents = isLinux && !isWeb;
|
||||
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
|
||||
// while `Alt` and `Control` are seperated key events for en-US input method.
|
||||
return FocusScope(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
canRequestFocus: true,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
onKey: (FocusNode data, RawKeyEvent e) =>
|
||||
inputModel.handleRawKeyEvent(e),
|
||||
onKey: useRawKeyEvents
|
||||
? (FocusNode data, RawKeyEvent event) =>
|
||||
inputModel.handleRawKeyEvent(event)
|
||||
: null,
|
||||
onKeyEvent: useRawKeyEvents
|
||||
? null
|
||||
: (FocusNode node, KeyEvent event) =>
|
||||
inputModel.handleKeyEvent(event),
|
||||
child: child));
|
||||
}
|
||||
}
|
||||
@@ -233,7 +243,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||
}
|
||||
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
||||
@@ -275,7 +285,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
ffi.cursorModel.clearRemoteWindowCoords();
|
||||
}
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -22,6 +23,20 @@ class TTextMenu {
|
||||
required this.onPressed,
|
||||
this.trailingIcon,
|
||||
this.divider = false});
|
||||
|
||||
Widget getChild() {
|
||||
if (trailingIcon != null) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
child,
|
||||
trailingIcon!,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TRadioMenu<T> {
|
||||
@@ -115,12 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// paste
|
||||
if (isMobile &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
perms['keyboard'] != false &&
|
||||
perms['clipboard'] != false) {
|
||||
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Paste')),
|
||||
child: Text(translate('Send clipboard keystrokes')),
|
||||
onPressed: () async {
|
||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
@@ -636,6 +648,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||
v.addAll(toolbarKeyboardToggles(ffi));
|
||||
}
|
||||
|
||||
// view mode (mobile only, desktop is in keyboard menu)
|
||||
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
|
||||
v.add(TToggleMenu(
|
||||
value: ffiModel.viewOnly,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, value);
|
||||
},
|
||||
child: Text(translate('View Mode'))));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -776,3 +800,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
bool showVirtualDisplayMenu(FFI ffi) {
|
||||
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||
return false;
|
||||
}
|
||||
if (!ffi.ffiModel.pi.isInstalled) {
|
||||
return false;
|
||||
}
|
||||
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Widget> getVirtualDisplayMenuChildren(
|
||||
FFI ffi, String id, VoidCallback? clickCallBack) {
|
||||
if (!showVirtualDisplayMenu(ffi)) {
|
||||
return [];
|
||||
}
|
||||
final pi = ffi.ffiModel.pi;
|
||||
final privacyModeState = PrivacyModeState.find(id);
|
||||
if (pi.isRustDeskIdd) {
|
||||
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
||||
children.add(Obx(() => CkbMenuButton(
|
||||
value: virtualDisplays.contains(i + 1),
|
||||
onChanged: privacyModeState.isNotEmpty
|
||||
? null
|
||||
: (bool? value) async {
|
||||
if (value != null) {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId, index: i + 1, on: value);
|
||||
clickCallBack?.call();
|
||||
}
|
||||
},
|
||||
child: Text('${translate('Virtual display')} ${i + 1}'),
|
||||
ffi: ffi,
|
||||
)));
|
||||
}
|
||||
children.add(Divider());
|
||||
children.add(Obx(() => MenuButton(
|
||||
onPressed: privacyModeState.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false);
|
||||
clickCallBack?.call();
|
||||
},
|
||||
ffi: ffi,
|
||||
child: Text(translate('Plug out all')),
|
||||
)));
|
||||
return children;
|
||||
}
|
||||
if (pi.isAmyuniIdd) {
|
||||
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
||||
final children = <Widget>[
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId, index: 0, on: false);
|
||||
clickCallBack?.call();
|
||||
},
|
||||
child: Icon(Icons.remove),
|
||||
),
|
||||
Text(count.toString()),
|
||||
TextButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 4
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId, index: 0, on: true);
|
||||
clickCallBack?.call();
|
||||
},
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
)),
|
||||
Divider(),
|
||||
Obx(() => MenuButton(
|
||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||
? null
|
||||
: () {
|
||||
bind.sessionToggleVirtualDisplay(
|
||||
sessionId: ffi.sessionId,
|
||||
index: kAllVirtualDisplay,
|
||||
on: false);
|
||||
clickCallBack?.call();
|
||||
},
|
||||
ffi: ffi,
|
||||
child: Text(translate('Plug out all')),
|
||||
)),
|
||||
];
|
||||
return children;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
const String kPeerPlatformMacOS = "Mac OS";
|
||||
const String kPeerPlatformAndroid = "Android";
|
||||
const String kPeerPlatformWebDesktop = "WebDesktop";
|
||||
|
||||
const double kScrollbarThickness = 12.0;
|
||||
|
||||
@@ -135,6 +136,18 @@ const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||
const String kOptionStopService = "stop-service";
|
||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||
|
||||
// buildin opitons
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||
const String kOptionRemovePresetPasswordWarning =
|
||||
"remove-preset-password-warning";
|
||||
const kHideUsernameOnCard = "hide-username-on-card";
|
||||
const String kOptionHideHelpCards = "hide-help-cards";
|
||||
|
||||
const String kOptionToggleViewOnly = "view-only";
|
||||
|
||||
@@ -155,6 +168,8 @@ const int kWindowMainId = 0;
|
||||
const String kPointerEventKindTouch = "touch";
|
||||
const String kPointerEventKindMouse = "mouse";
|
||||
|
||||
const String kKeyFlutterKey = "flutter_key";
|
||||
|
||||
const String kKeyShowDisplaysAsIndividualWindows =
|
||||
'displays_as_individual_windows';
|
||||
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||
@@ -227,9 +242,9 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
|
||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||
const kFullScreenEdgeSize = 0.0;
|
||||
const kMaximizeEdgeSize = 0.0;
|
||||
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
||||
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||
final kWindowBorderWidth = 1.0;
|
||||
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
|
||||
const kWindowResizeEdgeSize = 5.0;
|
||||
const kWindowBorderWidth = 1.0;
|
||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||
const kFrameBorderRadius = 12.0;
|
||||
const kFrameClipRRectBorderRadius = 12.0;
|
||||
|
||||
@@ -169,16 +169,12 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
|
||||
final status =
|
||||
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
||||
final statusNum = status['status_num'] as int;
|
||||
final preStatus = stateGlobal.svcStatus.value;
|
||||
if (statusNum == 0) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
||||
} else if (statusNum == -1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
} else if (statusNum == 1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.ready;
|
||||
if (preStatus != SvcStatus.ready) {
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
}
|
||||
} else {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
}
|
||||
@@ -212,14 +208,14 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
if (lastRemoteId != _idController.id) {
|
||||
setState(() {
|
||||
_idController.id = lastRemoteId;
|
||||
});
|
||||
}
|
||||
}();
|
||||
});
|
||||
}
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
windowManager.addListener(this);
|
||||
@@ -261,8 +257,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
@override
|
||||
void onWindowLeaveFullScreen() {
|
||||
// Restore edge border to default edge size.
|
||||
stateGlobal.resizeEdgeSize.value =
|
||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
|
||||
stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
|
||||
? kMaximizeEdgeSize
|
||||
: windowResizeEdgeSize;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -340,7 +337,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
?.merge(TextStyle(height: 1)),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 0),
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: translate("id_input_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
|
||||
@@ -443,14 +443,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
});
|
||||
}
|
||||
} else if (isMacOS) {
|
||||
if (!(bind.isOutgoingOnly() ||
|
||||
bind.mainIsCanScreenRecording(prompt: false))) {
|
||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||
if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
|
||||
return buildInstallCard("Permissions", "config_screen", "Configure",
|
||||
() async {
|
||||
bind.mainIsCanScreenRecording(prompt: true);
|
||||
watchIsCanScreenRecording = true;
|
||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||
} else if (!bind.mainIsProcessTrusted(prompt: false)) {
|
||||
} else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
|
||||
return buildInstallCard("Permissions", "config_acc", "Configure",
|
||||
() async {
|
||||
bind.mainIsProcessTrusted(prompt: true);
|
||||
@@ -462,7 +462,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
bind.mainIsCanInputMonitoring(prompt: true);
|
||||
watchIsInputMonitoring = true;
|
||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||
} else if (!svcStopped.value &&
|
||||
} else if (!isOutgoingOnly &&
|
||||
!svcStopped.value &&
|
||||
bind.mainIsInstalled() &&
|
||||
!bind.mainIsInstalledDaemon(prompt: false)) {
|
||||
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
||||
@@ -545,6 +546,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
String? link,
|
||||
bool? closeButton,
|
||||
String? closeOption}) {
|
||||
if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
|
||||
content != 'install_daemon_tip') {
|
||||
return const SizedBox();
|
||||
}
|
||||
void closeCard() async {
|
||||
if (closeOption != null) {
|
||||
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
||||
@@ -838,7 +843,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
}
|
||||
|
||||
void setPasswordDialog() async {
|
||||
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
final p0 = TextEditingController(text: pw);
|
||||
final p1 = TextEditingController(text: pw);
|
||||
@@ -852,6 +857,7 @@ void setPasswordDialog() async {
|
||||
// SpecialCharacterValidationRule(),
|
||||
MinCharactersValidationRule(8),
|
||||
];
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
@@ -878,6 +884,9 @@ void setPasswordDialog() async {
|
||||
return;
|
||||
}
|
||||
bind.mainSetPermanentPassword(password: pass);
|
||||
if (pass.isNotEmpty) {
|
||||
notEmptyCallback?.call();
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -907,6 +916,7 @@ void setPasswordDialog() async {
|
||||
errMsg0 = '';
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -933,6 +943,7 @@ void setPasswordDialog() async {
|
||||
errMsg1 = '';
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -61,9 +61,14 @@ class DesktopSettingPage extends StatefulWidget {
|
||||
final SettingsTabKey initialTabkey;
|
||||
static final List<SettingsTabKey> tabKeys = [
|
||||
SettingsTabKey.general,
|
||||
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
|
||||
if (!isWeb &&
|
||||
!bind.isOutgoingOnly() &&
|
||||
!bind.isDisableSettings() &&
|
||||
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
||||
SettingsTabKey.safety,
|
||||
if (!bind.isDisableSettings()) SettingsTabKey.network,
|
||||
if (!bind.isDisableSettings() &&
|
||||
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
|
||||
SettingsTabKey.network,
|
||||
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
||||
SettingsTabKey.plugin,
|
||||
@@ -74,7 +79,8 @@ class DesktopSettingPage extends StatefulWidget {
|
||||
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
|
||||
State<DesktopSettingPage> createState() =>
|
||||
_DesktopSettingPageState(initialTabkey);
|
||||
|
||||
static void switch2page(SettingsTabKey page) {
|
||||
try {
|
||||
@@ -84,8 +90,10 @@ class DesktopSettingPage extends StatefulWidget {
|
||||
}
|
||||
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
||||
DesktopTabPage.onAddSetting(initialPage: page);
|
||||
PageController controller = Get.find(tag: _kSettingPageControllerTag);
|
||||
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
|
||||
PageController controller =
|
||||
Get.find<PageController>(tag: _kSettingPageControllerTag);
|
||||
Rx<SettingsTabKey> selected =
|
||||
Get.find<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
|
||||
selected.value = page;
|
||||
controller.jumpToPage(index);
|
||||
} else {
|
||||
@@ -105,10 +113,8 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
|
||||
_DesktopSettingPageState(SettingsTabKey initialTabkey) {
|
||||
var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
|
||||
if (initialIndex == -1) {
|
||||
initialIndex = 0;
|
||||
}
|
||||
@@ -171,16 +177,32 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
}
|
||||
|
||||
List<Widget> _children() {
|
||||
final children = [
|
||||
_General(),
|
||||
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(),
|
||||
if (!bind.isDisableSettings()) _Network(),
|
||||
if (!bind.isIncomingOnly()) _Display(),
|
||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
||||
_Plugin(),
|
||||
if (!bind.isDisableAccount()) _Account(),
|
||||
_About(),
|
||||
];
|
||||
final children = List<Widget>.empty(growable: true);
|
||||
for (final tab in DesktopSettingPage.tabKeys) {
|
||||
switch (tab) {
|
||||
case SettingsTabKey.general:
|
||||
children.add(const _General());
|
||||
break;
|
||||
case SettingsTabKey.safety:
|
||||
children.add(const _Safety());
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -195,7 +217,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
width: _kTabWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
_header(),
|
||||
_header(context),
|
||||
Flexible(child: _listView(tabs: _settingTabs())),
|
||||
],
|
||||
),
|
||||
@@ -218,21 +240,40 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _header() {
|
||||
Widget _header(BuildContext context) {
|
||||
final settingsText = Text(
|
||||
translate('Settings'),
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: _accentColor,
|
||||
fontSize: _kTitleFontSize,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 62,
|
||||
child: Text(
|
||||
translate('Settings'),
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: _accentColor,
|
||||
fontSize: _kTitleFontSize,
|
||||
fontWeight: FontWeight.w400,
|
||||
if (isWeb)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.arrow_back),
|
||||
).marginOnly(left: 5),
|
||||
if (isWeb)
|
||||
SizedBox(
|
||||
height: 62,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: settingsText,
|
||||
),
|
||||
),
|
||||
).marginOnly(left: 20, top: 10),
|
||||
).marginOnly(left: 20),
|
||||
if (!isWeb)
|
||||
SizedBox(
|
||||
height: 62,
|
||||
child: settingsText,
|
||||
).marginOnly(left: 20, top: 10),
|
||||
const Spacer(),
|
||||
],
|
||||
);
|
||||
@@ -301,7 +342,8 @@ class _General extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GeneralState extends State<_General> {
|
||||
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service');
|
||||
final RxBool serviceStop =
|
||||
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
|
||||
RxBool serviceBtnEnabled = true.obs;
|
||||
|
||||
@override
|
||||
@@ -313,13 +355,13 @@ class _GeneralState extends State<_General> {
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
children: [
|
||||
service(),
|
||||
if (!isWeb) service(),
|
||||
theme(),
|
||||
_Card(title: 'Language', children: [language()]),
|
||||
hwcodec(),
|
||||
audio(context),
|
||||
record(context),
|
||||
WaylandCard(),
|
||||
if (!isWeb) hwcodec(),
|
||||
if (!isWeb) audio(context),
|
||||
if (!isWeb) record(context),
|
||||
if (!isWeb) WaylandCard(),
|
||||
other()
|
||||
],
|
||||
).marginOnly(bottom: _kListViewBottomMargin));
|
||||
@@ -373,13 +415,13 @@ class _GeneralState extends State<_General> {
|
||||
|
||||
Widget other() {
|
||||
final children = <Widget>[
|
||||
if (!bind.isIncomingOnly())
|
||||
if (!isWeb && !bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
kOptionEnableConfirmClosingTabs,
|
||||
isServer: false),
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||
wallpaper(),
|
||||
if (!bind.isIncomingOnly()) ...[
|
||||
if (!isWeb) wallpaper(),
|
||||
if (!isWeb && !bind.isIncomingOnly()) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Open connection in new tab',
|
||||
@@ -396,18 +438,19 @@ class _GeneralState extends State<_General> {
|
||||
kOptionAllowAlwaysSoftwareRender,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: translate('texture_render_tip'),
|
||||
child: _OptionCheckBox(
|
||||
context,
|
||||
"Use texture rendering",
|
||||
kOptionTextureRender,
|
||||
optGetter: bind.mainGetUseTextureRender,
|
||||
optSetter: (k, v) async =>
|
||||
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
|
||||
if (!isWeb)
|
||||
Tooltip(
|
||||
message: translate('texture_render_tip'),
|
||||
child: _OptionCheckBox(
|
||||
context,
|
||||
"Use texture rendering",
|
||||
kOptionTextureRender,
|
||||
optGetter: bind.mainGetUseTextureRender,
|
||||
optSetter: (k, v) async =>
|
||||
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!bind.isCustomClient())
|
||||
if (!isWeb && !bind.isCustomClient())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Check for software update on startup',
|
||||
@@ -422,7 +465,7 @@ class _GeneralState extends State<_General> {
|
||||
)
|
||||
],
|
||||
];
|
||||
if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||
children.add(_OptionCheckBox(
|
||||
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||
}
|
||||
@@ -493,18 +536,20 @@ class _GeneralState extends State<_General> {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
return AudioInput(builder: (devices, currentDevice, setDevice) {
|
||||
return _Card(title: 'Audio Input Device', children: [
|
||||
...devices.map((device) => _Radio<String>(context,
|
||||
value: device,
|
||||
groupValue: currentDevice,
|
||||
autoNewLine: false,
|
||||
label: device, onChanged: (value) {
|
||||
setDevice(value);
|
||||
setState(() {});
|
||||
}))
|
||||
]);
|
||||
});
|
||||
builder(devices, currentDevice, setDevice) {
|
||||
final child = ComboBox(
|
||||
keys: devices,
|
||||
values: devices,
|
||||
initialKey: currentDevice,
|
||||
onChanged: (key) async {
|
||||
setDevice(key);
|
||||
setState(() {});
|
||||
},
|
||||
).marginOnly(left: _kContentHMargin);
|
||||
return _Card(title: 'Audio Input Device', children: [child]);
|
||||
}
|
||||
|
||||
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
|
||||
}
|
||||
|
||||
Widget record(BuildContext context) {
|
||||
@@ -618,8 +663,9 @@ class _GeneralState extends State<_General> {
|
||||
initialKey: currentKey,
|
||||
onChanged: (key) async {
|
||||
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
|
||||
reloadAllWindows();
|
||||
bind.mainChangeLanguage(lang: key);
|
||||
if (isWeb) reloadCurrentWindow();
|
||||
if (!isWeb) reloadAllWindows();
|
||||
if (!isWeb) bind.mainChangeLanguage(lang: key);
|
||||
},
|
||||
enabled: !isOptFixed,
|
||||
).marginOnly(left: _kContentHMargin);
|
||||
@@ -679,15 +725,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
// Simple temp wrapper for PR check
|
||||
tmpWrapper() {
|
||||
RxBool has2fa = bind.mainHasValid2FaSync().obs;
|
||||
RxBool hasBot = bind.mainHasValidBotSync().obs;
|
||||
update() async {
|
||||
has2fa.value = bind.mainHasValid2FaSync();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
onChanged(bool? checked) async {
|
||||
change2fa(callback: update);
|
||||
if (checked == false) {
|
||||
CommonConfirmDialog(
|
||||
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
||||
change2fa(callback: update);
|
||||
});
|
||||
} else {
|
||||
change2fa(callback: update);
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
final tfa = GestureDetector(
|
||||
child: InkWell(
|
||||
child: Obx(() => Row(
|
||||
children: [
|
||||
@@ -708,6 +763,77 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
onChanged(!has2fa.value);
|
||||
},
|
||||
).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();
|
||||
@@ -832,12 +958,22 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
label: value,
|
||||
onChanged: locked
|
||||
? null
|
||||
: ((value) {
|
||||
() async {
|
||||
: ((value) async {
|
||||
callback() async {
|
||||
await model.setVerificationMethod(
|
||||
passwordKeys[passwordValues.indexOf(value)]);
|
||||
await model.updatePasswordModel();
|
||||
}();
|
||||
}
|
||||
|
||||
if (value ==
|
||||
passwordValues[passwordKeys
|
||||
.indexOf(kUsePermanentPassword)] &&
|
||||
(await bind.mainGetPermanentPassword())
|
||||
.isEmpty) {
|
||||
setPasswordDialog(notEmptyCallback: callback);
|
||||
} else {
|
||||
await callback();
|
||||
}
|
||||
}),
|
||||
))
|
||||
.toList();
|
||||
@@ -930,6 +1066,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
|
||||
'allow-only-conn-window-open',
|
||||
reverse: false, enabled: enabled),
|
||||
if (bind.mainIsInstalled()) unlockPin()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1030,12 +1167,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
bool enabled = !locked;
|
||||
// Simple temp wrapper for PR check
|
||||
tmpWrapper() {
|
||||
RxBool hasWhitelist = (bind.mainGetOptionSync(key: kOptionWhitelist) !=
|
||||
defaultOptionWhitelist)
|
||||
.obs;
|
||||
RxBool hasWhitelist = whitelistNotEmpty().obs;
|
||||
update() async {
|
||||
hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) !=
|
||||
defaultOptionWhitelist;
|
||||
hasWhitelist.value = whitelistNotEmpty();
|
||||
}
|
||||
|
||||
onChanged(bool? checked) async {
|
||||
@@ -1146,7 +1280,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
width: 95,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
enabled: enabled && !locked && isOptFixed,
|
||||
enabled: enabled && !locked && !isOptFixed,
|
||||
onChanged: (_) => applyEnabled.value = true,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(
|
||||
@@ -1180,6 +1314,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 {
|
||||
@@ -1192,13 +1360,18 @@ class _Network extends StatefulWidget {
|
||||
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
bool locked = bind.mainIsInstalled();
|
||||
bool locked = !isWeb && bind.mainIsInstalled();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
bool enabled = !locked;
|
||||
final scrollController = ScrollController();
|
||||
final hideServer =
|
||||
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||
// TODO: support web proxy
|
||||
final hideProxy =
|
||||
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: ListView(
|
||||
@@ -1212,11 +1385,12 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
AbsorbPointer(
|
||||
absorbing: locked,
|
||||
child: Column(children: [
|
||||
server(enabled),
|
||||
_Card(title: 'Proxy', children: [
|
||||
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
||||
enabled: enabled),
|
||||
]),
|
||||
if (!hideServer) server(enabled),
|
||||
if (!hideProxy)
|
||||
_Card(title: 'Proxy', children: [
|
||||
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
||||
enabled: enabled),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin));
|
||||
@@ -1317,7 +1491,7 @@ class _DisplayState extends State<_Display> {
|
||||
scrollStyle(context),
|
||||
imageQuality(context),
|
||||
codec(context),
|
||||
privacyModeImpl(context),
|
||||
if (!isWeb) privacyModeImpl(context),
|
||||
other(context),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin));
|
||||
}
|
||||
@@ -1715,7 +1889,7 @@ class _AboutState extends State<_About> {
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
child: _Card(title: '${translate('About')} RustDesk', children: [
|
||||
child: _Card(title: translate('About RustDesk'), children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1728,9 +1902,10 @@ class _AboutState extends State<_About> {
|
||||
SelectionArea(
|
||||
child: Text('${translate('Build Date')}: $buildDate')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
SelectionArea(
|
||||
child: Text('${translate('Fingerprint')}: $fingerprint')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
if (!isWeb)
|
||||
SelectionArea(
|
||||
child: Text('${translate('Fingerprint')}: $fingerprint')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com/privacy.html');
|
||||
@@ -2070,9 +2245,14 @@ Widget _lock(
|
||||
Text(translate(label)).marginOnly(left: 5),
|
||||
]).marginSymmetric(vertical: 2)),
|
||||
onPressed: () async {
|
||||
bool checked = await callMainCheckSuperUserPermission();
|
||||
if (checked) {
|
||||
onUnlock();
|
||||
final unlockPin = bind.mainGetUnlockPin();
|
||||
if (unlockPin.isEmpty) {
|
||||
bool checked = await callMainCheckSuperUserPermission();
|
||||
if (checked) {
|
||||
onUnlock();
|
||||
}
|
||||
} else {
|
||||
checkUnlockPinDialog(unlockPin, onUnlock);
|
||||
}
|
||||
},
|
||||
).marginSymmetric(horizontal: 2, vertical: 4),
|
||||
@@ -2249,35 +2429,40 @@ void changeSocks5Proxy() async {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('Server'),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 0),
|
||||
message: translate("default_proxy_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
if (!isMobile)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('Server'),
|
||||
).marginOnly(right: 4),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 0),
|
||||
message: translate("default_proxy_tip"),
|
||||
child: Icon(
|
||||
Icons.help_outline_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)).marginOnly(right: 10),
|
||||
),
|
||||
],
|
||||
)).marginOnly(right: 10),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
|
||||
labelText: isMobile ? translate('Server') : null,
|
||||
helperText:
|
||||
isMobile ? translate("default_proxy_tip") : null,
|
||||
helperMaxLines: isMobile ? 3 : null,
|
||||
),
|
||||
controller: proxyController,
|
||||
autofocus: true,
|
||||
@@ -2288,15 +2473,19 @@ void changeSocks5Proxy() async {
|
||||
).marginOnly(bottom: 8),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
'${translate("Username")}:',
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10)),
|
||||
if (!isMobile)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
'${translate("Username")}:',
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10)),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: userController,
|
||||
decoration: InputDecoration(
|
||||
labelText: isMobile ? translate('Username') : null,
|
||||
),
|
||||
enabled: !isOptFixed,
|
||||
),
|
||||
),
|
||||
@@ -2304,16 +2493,18 @@ void changeSocks5Proxy() async {
|
||||
).marginOnly(bottom: 8),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
'${translate("Password")}:',
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10)),
|
||||
if (!isMobile)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
'${translate("Password")}:',
|
||||
textAlign: TextAlign.right,
|
||||
).marginOnly(right: 10)),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: obscure.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: isMobile ? translate('Password') : null,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => obscure.value = !obscure.value,
|
||||
icon: Icon(obscure.value
|
||||
@@ -2321,6 +2512,7 @@ void changeSocks5Proxy() async {
|
||||
: Icons.visibility))),
|
||||
controller: pwdController,
|
||||
enabled: !isOptFixed,
|
||||
maxLength: bind.mainMaxEncryptLen(),
|
||||
)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
|
||||
import '../../common/shared_state.dart';
|
||||
|
||||
@@ -20,7 +21,7 @@ class DesktopTabPage extends StatefulWidget {
|
||||
static void onAddSetting(
|
||||
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
||||
try {
|
||||
DesktopTabController tabController = Get.find();
|
||||
DesktopTabController tabController = Get.find<DesktopTabController>();
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelSettingPage,
|
||||
label: kTabLabelSettingPage,
|
||||
@@ -41,21 +42,11 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||
|
||||
final RxBool _block = false.obs;
|
||||
// bool mouseIn = false;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
shouldBeBlocked(_block, canBeBlocked);
|
||||
} else if (state == AppLifecycleState.inactive) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
_DesktopTabPageState() {
|
||||
RemoteCountState.init();
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelHomePage,
|
||||
label: kTabLabelHomePage,
|
||||
@@ -78,8 +69,34 @@ 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
|
||||
void dispose() {
|
||||
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
Get.delete<DesktopTabController>();
|
||||
|
||||
@@ -102,18 +119,15 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
||||
isClose: false,
|
||||
),
|
||||
),
|
||||
blockTab: _block,
|
||||
)));
|
||||
widget() => MouseRegion(
|
||||
onEnter: (_) async {
|
||||
await shouldBeBlocked(_block, canBeBlocked);
|
||||
},
|
||||
child: FocusScope(child: tabWidget, canRequestFocus: !_block.value));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? Obx(() => widget())
|
||||
? tabWidget
|
||||
: Obx(
|
||||
() => DragToResizeArea(
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
child: widget(),
|
||||
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||
child: tabWidget,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
@@ -68,7 +69,7 @@ class FileManagerPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FileManagerPageState extends State<FileManagerPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
||||
|
||||
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
||||
@@ -92,13 +93,17 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||
Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
debugPrint("File manager page init success with id ${widget.id}");
|
||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -111,12 +116,21 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||
});
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
jobController.jobTable.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@@ -170,10 +184,25 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
/// transfer status list
|
||||
/// watch transfer status
|
||||
Widget statusList() {
|
||||
Widget getIcon(JobProgress job) {
|
||||
final color = Theme.of(context).tabBarTheme.labelColor;
|
||||
switch (job.type) {
|
||||
case JobType.deleteDir:
|
||||
case JobType.deleteFile:
|
||||
return Icon(Icons.delete_outline, color: color);
|
||||
default:
|
||||
return Transform.rotate(
|
||||
angle: job.isRemoteToLocal ? pi : 0,
|
||||
child: Icon(Icons.arrow_forward_ios, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
statusListView(List<JobProgress> jobs) => ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = jobs[index];
|
||||
final status = item.getStatus();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
child: generateCard(
|
||||
@@ -183,15 +212,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemoteToLocal ? pi : 0,
|
||||
child: SvgPicture.asset("assets/arrow.svg",
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context).tabBarTheme.labelColor)),
|
||||
).paddingOnly(left: 15),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
getIcon(item)
|
||||
.marginSymmetric(horizontal: 10, vertical: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -200,45 +222,28 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: item.jobName,
|
||||
child: Text(
|
||||
item.fileName,
|
||||
child: ExtendedText(
|
||||
item.jobName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
Text(
|
||||
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
overflowWidget: TextOverflowWidget(
|
||||
child: Text("..."),
|
||||
position: TextOverflowPosition.start),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state != JobState.inProgress,
|
||||
child: Text(
|
||||
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: status,
|
||||
child: Text(status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
)).marginOnly(top: 6),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state == JobState.inProgress,
|
||||
child: Text(
|
||||
translate(
|
||||
item.display(),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state != JobState.inProgress,
|
||||
offstage: item.type != JobType.transfer ||
|
||||
item.state != JobState.inProgress,
|
||||
child: LinearPercentIndicator(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
animateFromLastPercent: true,
|
||||
center: Text(
|
||||
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
||||
@@ -248,7 +253,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
progressColor: MyTheme.accent,
|
||||
backgroundColor: Theme.of(context).hoverColor,
|
||||
lineHeight: kDesktopFileTransferRowHeight,
|
||||
).paddingSymmetric(vertical: 15),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -259,6 +264,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Offstage(
|
||||
offstage: item.state != JobState.paused,
|
||||
child: MenuButton(
|
||||
tooltip: translate("Resume"),
|
||||
onPressed: () {
|
||||
jobController.resumeJob(item.id);
|
||||
},
|
||||
@@ -271,7 +277,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
),
|
||||
),
|
||||
MenuButton(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
tooltip: translate("Delete"),
|
||||
child: SvgPicture.asset(
|
||||
"assets/close.svg",
|
||||
colorFilter: svgColor(Colors.white),
|
||||
@@ -284,11 +290,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
hoverColor: MyTheme.accent80,
|
||||
),
|
||||
],
|
||||
),
|
||||
).marginAll(12),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -518,6 +524,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
Row(
|
||||
children: [
|
||||
MenuButton(
|
||||
tooltip: translate('Back'),
|
||||
padding: EdgeInsets.only(
|
||||
right: 3,
|
||||
),
|
||||
@@ -537,6 +544,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate('Parent directory'),
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: SvgPicture.asset(
|
||||
@@ -601,6 +609,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
switch (_locationStatus.value) {
|
||||
case LocationStatus.bread:
|
||||
return MenuButton(
|
||||
tooltip: translate('Search'),
|
||||
onPressed: () {
|
||||
_locationStatus.value = LocationStatus.fileSearchBar;
|
||||
Future.delayed(
|
||||
@@ -627,6 +636,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
);
|
||||
case LocationStatus.fileSearchBar:
|
||||
return MenuButton(
|
||||
tooltip: translate('Clear'),
|
||||
onPressed: () {
|
||||
onSearchText("", isLocal);
|
||||
_locationStatus.value = LocationStatus.bread;
|
||||
@@ -642,6 +652,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
}
|
||||
}),
|
||||
MenuButton(
|
||||
tooltip: translate('Refresh File'),
|
||||
padding: EdgeInsets.only(
|
||||
left: 3,
|
||||
),
|
||||
@@ -667,6 +678,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||
children: [
|
||||
MenuButton(
|
||||
tooltip: translate('Home'),
|
||||
padding: EdgeInsets.only(
|
||||
right: 3,
|
||||
),
|
||||
@@ -682,11 +694,27 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate('Create Folder'),
|
||||
onPressed: () {
|
||||
final name = TextEditingController();
|
||||
String? errorText;
|
||||
_ffi.dialogManager.show((setState, close, context) {
|
||||
name.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
submit() {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
if (!PathUtil.validName(name.value.text,
|
||||
controller.options.value.isWindows)) {
|
||||
setState(() {
|
||||
errorText = translate("Invalid folder name");
|
||||
});
|
||||
return;
|
||||
}
|
||||
controller.createDir(PathUtil.join(
|
||||
controller.directory.value.path,
|
||||
name.value.text,
|
||||
@@ -718,6 +746,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
labelText: translate(
|
||||
"Please enter the folder name",
|
||||
),
|
||||
errorText: errorText,
|
||||
),
|
||||
controller: name,
|
||||
autofocus: true,
|
||||
@@ -751,6 +780,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
),
|
||||
Obx(() => MenuButton(
|
||||
tooltip: translate('Delete'),
|
||||
onPressed: SelectedItems.valid(selectedItems.items)
|
||||
? () async {
|
||||
await (controller
|
||||
@@ -882,6 +912,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
child: MenuButton(
|
||||
tooltip: translate('More'),
|
||||
onPressed: () => mod_menu.showMenu(
|
||||
context: context,
|
||||
position: menuPos,
|
||||
@@ -913,6 +944,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
BuildContext context, ScrollController scrollController) {
|
||||
final fd = controller.directory.value;
|
||||
final entries = fd.entries;
|
||||
Rx<Entry?> rightClickEntry = Rx(null);
|
||||
|
||||
return ListSearchActionListener(
|
||||
node: _keyboardNode,
|
||||
@@ -971,16 +1003,70 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
final lastModifiedStr = entry.isDrive
|
||||
? " "
|
||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
onTap() {
|
||||
final items = selectedItems;
|
||||
// handle double click
|
||||
if (_checkDoubleClick(entry)) {
|
||||
controller.openDirectory(entry.path);
|
||||
items.clear();
|
||||
return;
|
||||
}
|
||||
_onSelectedChanged(items, filteredEntries, entry, isLocal);
|
||||
}
|
||||
|
||||
onSecondaryTap() {
|
||||
final items = [
|
||||
if (!entry.isDrive &&
|
||||
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
|
||||
mod_menu.PopupMenuItem(
|
||||
child: Text("Rename"),
|
||||
height: CustomPopupMenuTheme.height,
|
||||
onTap: () {
|
||||
controller.renameAction(entry, isLocal);
|
||||
},
|
||||
)
|
||||
];
|
||||
if (items.isNotEmpty) {
|
||||
rightClickEntry.value = entry;
|
||||
final future = mod_menu.showMenu(
|
||||
context: context,
|
||||
position: secondaryPosition,
|
||||
items: items,
|
||||
);
|
||||
future.then((value) {
|
||||
rightClickEntry.value = null;
|
||||
});
|
||||
future.onError((error, stackTrace) {
|
||||
rightClickEntry.value = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSecondaryTapDown(details) {
|
||||
secondaryPosition = RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 1),
|
||||
child: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: selectedItems.items.contains(entry)
|
||||
? Theme.of(context).hoverColor
|
||||
? MyTheme.button
|
||||
: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5.0),
|
||||
),
|
||||
border: rightClickEntry.value == entry
|
||||
? Border.all(
|
||||
color: MyTheme.button,
|
||||
width: 1.0,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
key: ValueKey(entry.name),
|
||||
height: kDesktopFileTransferRowHeight,
|
||||
@@ -1019,22 +1105,19 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
),
|
||||
Expanded(
|
||||
child: Text(entry.name.nonBreaking,
|
||||
style: TextStyle(
|
||||
color: selectedItems.items
|
||||
.contains(entry)
|
||||
? Colors.white
|
||||
: null),
|
||||
overflow:
|
||||
TextOverflow.ellipsis))
|
||||
]),
|
||||
)),
|
||||
),
|
||||
onTap: () {
|
||||
final items = selectedItems;
|
||||
// handle double click
|
||||
if (_checkDoubleClick(entry)) {
|
||||
controller.openDirectory(entry.path);
|
||||
items.clear();
|
||||
return;
|
||||
}
|
||||
_onSelectedChanged(
|
||||
items, filteredEntries, entry, isLocal);
|
||||
},
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2.0,
|
||||
@@ -1051,11 +1134,17 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
color: selectedItems.items
|
||||
.contains(entry)
|
||||
? Colors.white70
|
||||
: MyTheme.darkGray,
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
// Divider from header.
|
||||
SizedBox(
|
||||
@@ -1071,9 +1160,16 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
sizeStr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10, color: MyTheme.darkGray),
|
||||
fontSize: 10,
|
||||
color:
|
||||
selectedItems.items.contains(entry)
|
||||
? Colors.white70
|
||||
: MyTheme.darkGray),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -34,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@@ -54,8 +55,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
||||
@@ -97,6 +96,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
));
|
||||
final tabWidget = isLinux
|
||||
@@ -111,6 +111,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
|
||||
class _InstallPageState extends State<InstallPage> {
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_InstallPageState() {
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
const label = "install";
|
||||
tabController.add(TabInfo(
|
||||
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return DragToResizeArea(
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||
child: Container(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
||||
);
|
||||
|
||||
_InstallPageBodyState() {
|
||||
controller = TextEditingController(text: bind.installInstallPath());
|
||||
final installOptions = jsonDecode(bind.installInstallOptions());
|
||||
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
|
||||
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
controller = TextEditingController(text: bind.installInstallPath());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
if (desktopicon.value) args += ' desktopicon';
|
||||
bind.installInstallMe(options: args, path: controller.text);
|
||||
}
|
||||
|
||||
do_install();
|
||||
}
|
||||
|
||||
|
||||
@@ -63,9 +63,12 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
isRdp: widget.isRDP);
|
||||
Get.put(_ffi, tag: 'pf_${widget.id}');
|
||||
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||
debugPrint("Port forward page init success with id ${widget.id}");
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -34,6 +34,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@@ -54,8 +55,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
debugPrint(
|
||||
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
@@ -106,6 +105,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
return true;
|
||||
},
|
||||
tail: AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
),
|
||||
);
|
||||
@@ -127,6 +127,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
() => SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -45,7 +45,9 @@ class RemotePage extends StatefulWidget {
|
||||
this.switchUuid,
|
||||
this.forceRelay,
|
||||
this.isSharedPassword,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key) {
|
||||
initSharedStates(id);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final SessionID? sessionId;
|
||||
@@ -64,7 +66,7 @@ class RemotePage extends StatefulWidget {
|
||||
|
||||
@override
|
||||
State<RemotePage> createState() {
|
||||
final state = _RemotePageState();
|
||||
final state = _RemotePageState(id);
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
@@ -94,8 +96,11 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
SessionID get sessionId => _ffi.sessionId;
|
||||
|
||||
_RemotePageState(String id) {
|
||||
_initStates(id);
|
||||
}
|
||||
|
||||
void _initStates(String id) {
|
||||
initSharedStates(id);
|
||||
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
@@ -105,9 +110,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initStates(widget.id);
|
||||
_ffi = FFI(widget.sessionId);
|
||||
Get.put(_ffi, tag: widget.id);
|
||||
Get.put<FFI>(_ffi, tag: widget.id);
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
showKBLayoutTypeChooserIfNeeded(
|
||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||
@@ -135,11 +139,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||
// Session option should be set after models.dart/FFI.start
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Session option should be set after models.dart/FFI.start
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||
});
|
||||
DesktopMultiWindow.addListener(this);
|
||||
// if (!_isCustomCursorInited) {
|
||||
// customCursorController.registerNeedUpdateCursorCallback(
|
||||
@@ -154,7 +160,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
// }
|
||||
|
||||
_blockableOverlayState.applyFfi(_ffi);
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -236,8 +245,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
super.dispose();
|
||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||
// ensure we leave this session, this is a double check
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
if (closeSession) {
|
||||
// ensure we leave this session, this is a double check
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
}
|
||||
DesktopMultiWindow.removeListener(this);
|
||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||
_ffi.imageModel.disposeImage();
|
||||
@@ -506,12 +517,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
];
|
||||
|
||||
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||
paints.add(Obx(() => Offstage(
|
||||
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
|
||||
child: CursorPaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
))));
|
||||
paints
|
||||
.add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
|
||||
? Offstage()
|
||||
: CursorPaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
)));
|
||||
}
|
||||
paints.add(
|
||||
Positioned(
|
||||
@@ -564,11 +576,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
|
||||
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
|
||||
@@ -71,7 +71,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final ffi = remotePage.ffi;
|
||||
bind.setCurSessionId(sessionId: ffi.sessionId);
|
||||
}
|
||||
WindowController.fromWindowId(windowId())
|
||||
WindowController.fromWindowId(params['windowId'])
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
UnreadChatCountState.find(id).value = 0;
|
||||
};
|
||||
@@ -98,15 +98,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
));
|
||||
_update_remote_count();
|
||||
}
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||
if (!_isScreenRectSet) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(
|
||||
@@ -121,11 +120,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = Scaffold(
|
||||
@@ -134,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
@@ -233,6 +228,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
// Specially configured for a better resize area and remote control.
|
||||
childPadding: kDragToResizeAreaPadding,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
));
|
||||
}
|
||||
@@ -413,12 +409,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final display = args['display'];
|
||||
final displays = args['displays'];
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
final prePeerCount = tabController.length;
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (stateGlobal.fullscreen.isTrue) {
|
||||
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
}
|
||||
await setNewConnectWindowFrame(windowId(), id!, display, screenRect);
|
||||
await setNewConnectWindowFrame(
|
||||
windowId(), id!, prePeerCount, display, screenRect);
|
||||
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||
await windowOnTop(windowId());
|
||||
});
|
||||
|
||||
@@ -32,14 +32,18 @@ class DesktopServerPage extends StatefulWidget {
|
||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
final tabController = gFFI.serverModel.tabController;
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
_DesktopServerPageState() {
|
||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
||||
windowManager.addListener(this);
|
||||
Get.put(tabController);
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
tabController.onRemoved = (_, id) {
|
||||
onRemoveId(id);
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -79,7 +83,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, serverModel, child) {
|
||||
final body = Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: ConnectionManager(),
|
||||
);
|
||||
return isLinux
|
||||
@@ -104,10 +108,11 @@ class ConnectionManager extends StatefulWidget {
|
||||
State<StatefulWidget> createState() => ConnectionManagerState();
|
||||
}
|
||||
|
||||
class ConnectionManagerState extends State<ConnectionManager> {
|
||||
@override
|
||||
void initState() {
|
||||
gFFI.serverModel.updateClientState();
|
||||
class ConnectionManagerState extends State<ConnectionManager>
|
||||
with WidgetsBindingObserver {
|
||||
final RxBool _block = false.obs;
|
||||
|
||||
ConnectionManagerState() {
|
||||
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
||||
final client_id = int.tryParse(client_id_str);
|
||||
if (client_id != null) {
|
||||
@@ -116,7 +121,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
if (client != null) {
|
||||
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
||||
if (client.unreadChatMessageCount.value > 0) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
client.unreadChatMessageCount.value = 0;
|
||||
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
||||
});
|
||||
@@ -127,9 +132,31 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
}
|
||||
};
|
||||
gFFI.chatModel.isConnManager = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (!allowRemoteCMModification()) {
|
||||
shouldBeBlocked(_block, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
gFFI.serverModel.updateClientState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final serverModel = Provider.of<ServerModel>(context);
|
||||
@@ -165,8 +192,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
maxLabelWidth: 100,
|
||||
tail: null, //buildScrollJumper(),
|
||||
selectedTabBackgroundColor:
|
||||
Theme.of(context).hintColor.withOpacity(0),
|
||||
blockTab: allowRemoteCMModification() ? null : _block,
|
||||
tabBuilder: (key, icon, label, themeConf) {
|
||||
final client = serverModel.clients
|
||||
.firstWhereOrNull((client) => client.id.toString() == key);
|
||||
@@ -201,27 +227,28 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
borderWidth;
|
||||
final realChatPageWidth =
|
||||
constrains.maxWidth - realClosedWidth;
|
||||
return Row(children: [
|
||||
final row = Row(children: [
|
||||
if (constrains.maxWidth >
|
||||
kConnectionManagerWindowSizeClosedChat.width)
|
||||
Consumer<ChatModel>(
|
||||
builder: (_, model, child) => SizedBox(
|
||||
width: realChatPageWidth,
|
||||
child: buildRemoteBlock(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.dividerColor))),
|
||||
child: buildSidePage()),
|
||||
),
|
||||
child: allowRemoteCMModification()
|
||||
? buildSidePage()
|
||||
: buildRemoteBlock(
|
||||
child: buildSidePage(),
|
||||
block: _block,
|
||||
mask: true),
|
||||
)),
|
||||
SizedBox(
|
||||
width: realClosedWidth,
|
||||
child:
|
||||
SizedBox(width: realClosedWidth, child: pageView)),
|
||||
]);
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: row,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -381,7 +408,10 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
_time.value = _time.value + 1;
|
||||
}
|
||||
});
|
||||
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -714,7 +744,8 @@ class _CmControlPanel extends StatelessWidget {
|
||||
child: buildButton(context,
|
||||
color: MyTheme.accent,
|
||||
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>;
|
||||
if (devices.isEmpty) {
|
||||
msgBox(
|
||||
@@ -740,13 +771,14 @@ class _CmControlPanel extends StatelessWidget {
|
||||
value: d,
|
||||
height: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: () => AudioInput.setDevice(d),
|
||||
onTap: () => AudioInput.setDevice(d, true, true),
|
||||
child: IgnorePointer(
|
||||
child: RadioMenuButton(
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) AudioInput.setDevice(v);
|
||||
if (v != null)
|
||||
AudioInput.setDevice(v, true, true);
|
||||
},
|
||||
child: Container(
|
||||
child: Text(
|
||||
@@ -1043,6 +1075,10 @@ class _CmControlPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
void checkClickTime(int id, Function() callback) async {
|
||||
if (allowRemoteCMModification()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
||||
await bind.cmCheckClickTime(connId: id);
|
||||
Timer(const Duration(milliseconds: 120), () async {
|
||||
@@ -1051,6 +1087,11 @@ void checkClickTime(int id, Function() callback) async {
|
||||
});
|
||||
}
|
||||
|
||||
bool allowRemoteCMModification() {
|
||||
return option2bool(kOptionAllowRemoteCmModification,
|
||||
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
|
||||
}
|
||||
|
||||
class _FileTransferLogPage extends StatefulWidget {
|
||||
_FileTransferLogPage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -1116,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
Text(translate('Create Folder'))
|
||||
],
|
||||
);
|
||||
case CmFileAction.rename:
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.drive_file_move_outlined,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
Text(translate('Rename'))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
|
||||
localPlatform = kPeerPlatformWindows;
|
||||
} else if (isLinux) {
|
||||
localPlatform = kPeerPlatformLinux;
|
||||
} else if (isWebOnWindows || isWebOnLinux) {
|
||||
localPlatform = kPeerPlatformWebDesktop;
|
||||
}
|
||||
// to-do: web desktop support ?
|
||||
return localPlatform;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: widget.tooltip,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
|
||||
@@ -38,18 +38,16 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
|
||||
@override
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(enabled?.value);
|
||||
}
|
||||
|
||||
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
extends State<W> {
|
||||
RxBool enabled = true.obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.enabled != null) {
|
||||
enabled.value = widget.enabled!.value;
|
||||
MyPopupMenuItemState(bool? e) {
|
||||
if (e != null) {
|
||||
enabled.value = e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -372,7 +372,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
initState() {
|
||||
super.initState();
|
||||
|
||||
Future.delayed(Duration.zero, () async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
_fractionX.value = double.tryParse(await bind.sessionGetOption(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
arg: 'remote-menubar-drag-x') ??
|
||||
@@ -452,8 +452,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
|
||||
Widget _buildToolbar(BuildContext context) {
|
||||
final List<Widget> toolbarItems = [];
|
||||
toolbarItems.add(_PinMenu(state: widget.state));
|
||||
if (!isWebDesktop) {
|
||||
toolbarItems.add(_PinMenu(state: widget.state));
|
||||
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
||||
}
|
||||
|
||||
@@ -1032,11 +1032,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
FFI get ffi => widget.ffi;
|
||||
String get id => widget.id;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_screenAdjustor.updateScreen();
|
||||
@@ -1052,15 +1047,11 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
ffi: widget.ffi,
|
||||
screenAdjustor: _screenAdjustor,
|
||||
),
|
||||
if (pi.isRustDeskIdd)
|
||||
_RustDeskVirtualDisplayMenu(
|
||||
id: widget.id,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
if (pi.isAmyuniIdd)
|
||||
_AmyuniVirtualDisplayMenu(
|
||||
id: widget.id,
|
||||
if (showVirtualDisplayMenu(ffi))
|
||||
_SubmenuButton(
|
||||
ffi: widget.ffi,
|
||||
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
|
||||
child: Text(translate("Virtual display")),
|
||||
),
|
||||
cursorToggles(),
|
||||
Divider(),
|
||||
@@ -1282,7 +1273,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getLocalResolutionWayland();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_getLocalResolutionWayland();
|
||||
});
|
||||
}
|
||||
|
||||
Rect? scaledRect() {
|
||||
@@ -1559,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 {
|
||||
final String id;
|
||||
final FFI ffi;
|
||||
@@ -1741,6 +1585,7 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
viewMode(),
|
||||
Divider(),
|
||||
...toolbarToggles(),
|
||||
...mobileActions(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1767,7 +1612,9 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
// If use flutter to grab keys, we can only use one mode.
|
||||
// Map mode and Legacy mode, at least one of them is supported.
|
||||
String? modeOnly;
|
||||
if (isInputSourceFlutter) {
|
||||
// Keep both map and legacy mode on web at the moment.
|
||||
// TODO: Remove legacy mode after web supports translate mode on web.
|
||||
if (isInputSourceFlutter && isDesktop) {
|
||||
if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
||||
modeOnly = kKeyMapMode;
|
||||
@@ -1871,12 +1718,47 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, value);
|
||||
final viewOnly = await bind.sessionGetToggleOption(
|
||||
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, viewOnly ?? value);
|
||||
}
|
||||
: null,
|
||||
ffi: ffi,
|
||||
child: Text(translate('View Mode')));
|
||||
}
|
||||
|
||||
mobileActions() {
|
||||
if (pi.platform != kPeerPlatformAndroid) return [];
|
||||
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
|
||||
if (!enabled) return [];
|
||||
return [
|
||||
Divider(),
|
||||
MenuButton(
|
||||
child: Text(translate('Back')),
|
||||
onPressed: () => ffi.inputModel.onMobileBack(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Home')),
|
||||
onPressed: () => ffi.inputModel.onMobileHome(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Apps')),
|
||||
onPressed: () => ffi.inputModel.onMobileApps(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Volume up')),
|
||||
onPressed: () => ffi.inputModel.onMobileVolumeUp(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Volume down')),
|
||||
onPressed: () => ffi.inputModel.onMobileVolumeDown(),
|
||||
ffi: ffi),
|
||||
MenuButton(
|
||||
child: Text(translate('Power')),
|
||||
onPressed: () => ffi.inputModel.onMobilePower(),
|
||||
ffi: ffi),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatMenu extends StatefulWidget {
|
||||
@@ -1950,28 +1832,31 @@ class _VoiceCallMenu extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
menuChildrenGetter() {
|
||||
final audioInput =
|
||||
AudioInput(builder: (devices, currentDevice, setDevice) {
|
||||
return Column(
|
||||
children: devices
|
||||
.map((d) => RdoMenuButton<String>(
|
||||
child: Container(
|
||||
child: Text(
|
||||
d,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
final audioInput = AudioInput(
|
||||
builder: (devices, currentDevice, setDevice) {
|
||||
return Column(
|
||||
children: devices
|
||||
.map((d) => RdoMenuButton<String>(
|
||||
child: Container(
|
||||
child: Text(
|
||||
d,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: 250),
|
||||
),
|
||||
constraints: BoxConstraints(maxWidth: 250),
|
||||
),
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) setDevice(v);
|
||||
},
|
||||
ffi: ffi,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) setDevice(v);
|
||||
},
|
||||
ffi: ffi,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
isCm: false,
|
||||
isVoiceCall: true,
|
||||
);
|
||||
return [
|
||||
audioInput,
|
||||
Divider(),
|
||||
|
||||
@@ -227,11 +227,9 @@ typedef TabMenuBuilder = Widget Function(String key);
|
||||
typedef LabelGetter = Rx<String> Function(String key);
|
||||
|
||||
/// [_lastClickTime], help to handle double click
|
||||
int _lastClickTime =
|
||||
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
||||
int _lastClickTime = 0;
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class DesktopTab extends StatelessWidget {
|
||||
class DesktopTab extends StatefulWidget {
|
||||
final bool showLogo;
|
||||
final bool showTitle;
|
||||
final bool showMinimize;
|
||||
@@ -248,15 +246,12 @@ class DesktopTab extends StatelessWidget {
|
||||
final Color? selectedTabBackgroundColor;
|
||||
final Color? unSelectedTabBackgroundColor;
|
||||
final Color? selectedBorderColor;
|
||||
final RxBool? blockTab;
|
||||
|
||||
final DesktopTabController controller;
|
||||
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
|
||||
|
||||
late final DesktopTabType tabType;
|
||||
late final bool isMainWindow;
|
||||
|
||||
final RxList<String> invisibleTabKeys = RxList.empty();
|
||||
|
||||
DesktopTab({
|
||||
@@ -277,38 +272,260 @@ class DesktopTab extends StatelessWidget {
|
||||
this.selectedTabBackgroundColor,
|
||||
this.unSelectedTabBackgroundColor,
|
||||
this.selectedBorderColor,
|
||||
}) : super(key: key) {
|
||||
tabType = controller.tabType;
|
||||
isMainWindow = tabType == DesktopTabType.main ||
|
||||
tabType == DesktopTabType.cm ||
|
||||
tabType == DesktopTabType.install;
|
||||
}
|
||||
this.blockTab,
|
||||
}) : super(key: key);
|
||||
|
||||
static RxString tablabelGetter(String peerId) {
|
||||
final alias = bind.mainGetPeerOptionSync(id: peerId, key: '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
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
Obx(() => Offstage(
|
||||
offstage: !stateGlobal.showTabBar.isTrue ||
|
||||
(kUseCompatibleUiMode && isHideSingleItem()),
|
||||
child: SizedBox(
|
||||
Obx(() {
|
||||
if (stateGlobal.showTabBar.isTrue &&
|
||||
!(kUseCompatibleUiMode && isHideSingleItem())) {
|
||||
final showBottomDivider = _showTabBarBottomDivider(tabType);
|
||||
return SizedBox(
|
||||
height: _kTabBarHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _kTabBarHeight - 1,
|
||||
height:
|
||||
showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight,
|
||||
child: _buildBar(),
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
if (showBottomDivider)
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
))),
|
||||
);
|
||||
} else {
|
||||
return Offstage();
|
||||
}
|
||||
}),
|
||||
Expanded(
|
||||
child: pageViewBuilder != null
|
||||
? pageViewBuilder!(_buildPageView())
|
||||
@@ -317,10 +534,15 @@ class DesktopTab extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildBlock({required Widget child}) {
|
||||
if (tabType != DesktopTabType.main) {
|
||||
if (blockTab != null) {
|
||||
return buildRemoteBlock(
|
||||
child: child,
|
||||
block: blockTab!,
|
||||
use: canBeBlocked,
|
||||
mask: tabType == DesktopTabType.main);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
return buildRemoteBlock(child: child, use: canBeBlocked);
|
||||
}
|
||||
|
||||
List<Widget> _tabWidgets = [];
|
||||
@@ -330,6 +552,13 @@ class DesktopTab extends StatelessWidget {
|
||||
controller: state.value.pageController,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: () {
|
||||
if (DesktopTabType.cm == tabType) {
|
||||
// Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful.
|
||||
return state.value.tabs.map((tab) {
|
||||
return tab.page;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// to-do refactor, separate connection state and UI state for remote session.
|
||||
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
||||
final tabLen = state.value.tabs.length;
|
||||
@@ -457,7 +686,6 @@ class DesktopTab extends StatelessWidget {
|
||||
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
|
||||
WindowActionPanel(
|
||||
isMainWindow: isMainWindow,
|
||||
tabType: tabType,
|
||||
state: state,
|
||||
tabController: controller,
|
||||
invisibleTabKeys: invisibleTabKeys,
|
||||
@@ -475,7 +703,6 @@ class DesktopTab extends StatelessWidget {
|
||||
|
||||
class WindowActionPanel extends StatefulWidget {
|
||||
final bool isMainWindow;
|
||||
final DesktopTabType tabType;
|
||||
final Rx<DesktopTabState> state;
|
||||
final DesktopTabController tabController;
|
||||
|
||||
@@ -491,7 +718,6 @@ class WindowActionPanel extends StatefulWidget {
|
||||
const WindowActionPanel(
|
||||
{Key? key,
|
||||
required this.isMainWindow,
|
||||
required this.tabType,
|
||||
required this.state,
|
||||
required this.tabController,
|
||||
required this.invisibleTabKeys,
|
||||
@@ -509,180 +735,7 @@ class WindowActionPanel extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
class WindowActionPanelState extends State<WindowActionPanel> {
|
||||
bool showTabDowndown() {
|
||||
return widget.tabController.state.value.tabs.length > 1 &&
|
||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||
@@ -703,72 +756,69 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Obx(() => Offstage(
|
||||
offstage:
|
||||
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
|
||||
child: _TabDropDownButton(
|
||||
controller: widget.tabController,
|
||||
labelGetter: widget.labelGetter,
|
||||
tabkeys: existingInvisibleTab()),
|
||||
)),
|
||||
Offstage(offstage: widget.tail == null, child: widget.tail),
|
||||
Offstage(
|
||||
offstage: kUseCompatibleUiMode,
|
||||
child: Row(
|
||||
Obx(() {
|
||||
if (showTabDowndown() && existingInvisibleTab().isNotEmpty) {
|
||||
return _TabDropDownButton(
|
||||
controller: widget.tabController,
|
||||
labelGetter: widget.labelGetter,
|
||||
tabkeys: existingInvisibleTab());
|
||||
} else {
|
||||
return Offstage();
|
||||
}
|
||||
}),
|
||||
if (widget.tail != null) widget.tail!,
|
||||
if (!kUseCompatibleUiMode)
|
||||
Row(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !widget.showMinimize || isMacOS,
|
||||
child: ActionIcon(
|
||||
message: 'Minimize',
|
||||
icon: IconFont.min,
|
||||
onTap: () {
|
||||
if (widget.isMainWindow) {
|
||||
windowManager.minimize();
|
||||
} else {
|
||||
WindowController.fromWindowId(kWindowId!).minimize();
|
||||
}
|
||||
},
|
||||
isClose: false,
|
||||
)),
|
||||
Offstage(
|
||||
offstage: !widget.showMaximize || isMacOS,
|
||||
child: Obx(() => ActionIcon(
|
||||
message: stateGlobal.isMaximized.isTrue
|
||||
? 'Restore'
|
||||
: 'Maximize',
|
||||
icon: stateGlobal.isMaximized.isTrue
|
||||
? IconFont.restore
|
||||
: IconFont.max,
|
||||
onTap: bind.isIncomingOnly() && isInHomePage()
|
||||
? null
|
||||
: _toggleMaximize,
|
||||
isClose: false,
|
||||
))),
|
||||
Offstage(
|
||||
offstage: !widget.showClose || isMacOS,
|
||||
child: ActionIcon(
|
||||
message: 'Close',
|
||||
icon: IconFont.close,
|
||||
onTap: () async {
|
||||
final res = await widget.onClose?.call() ?? true;
|
||||
if (res) {
|
||||
// hide for all window
|
||||
// note: the main window can be restored by tray icon
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (widget.isMainWindow) {
|
||||
await windowManager.close();
|
||||
} else {
|
||||
await WindowController.fromWindowId(kWindowId!)
|
||||
.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
isClose: true,
|
||||
))
|
||||
if (widget.showMinimize && !isMacOS)
|
||||
ActionIcon(
|
||||
message: 'Minimize',
|
||||
icon: IconFont.min,
|
||||
onTap: () {
|
||||
if (widget.isMainWindow) {
|
||||
windowManager.minimize();
|
||||
} else {
|
||||
WindowController.fromWindowId(kWindowId!).minimize();
|
||||
}
|
||||
},
|
||||
isClose: false,
|
||||
),
|
||||
if (widget.showMaximize && !isMacOS)
|
||||
Obx(() => ActionIcon(
|
||||
message: stateGlobal.isMaximized.isTrue
|
||||
? 'Restore'
|
||||
: 'Maximize',
|
||||
icon: stateGlobal.isMaximized.isTrue
|
||||
? IconFont.restore
|
||||
: IconFont.max,
|
||||
onTap: bind.isIncomingOnly() && isInHomePage()
|
||||
? null
|
||||
: _toggleMaximize,
|
||||
isClose: false,
|
||||
)),
|
||||
if (widget.showClose && !isMacOS)
|
||||
ActionIcon(
|
||||
message: 'Close',
|
||||
icon: IconFont.close,
|
||||
onTap: () async {
|
||||
final res = await widget.onClose?.call() ?? true;
|
||||
if (res) {
|
||||
// hide for all window
|
||||
// note: the main window can be restored by tray icon
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (widget.isMainWindow) {
|
||||
await windowManager.close();
|
||||
} else {
|
||||
await WindowController.fromWindowId(kWindowId!)
|
||||
.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
isClose: true,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1121,7 +1171,10 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _kTabBarHeight,
|
||||
// _kTabBarHeight also displays normally
|
||||
height: _showTabBarBottomDivider(widget.tabType)
|
||||
? _kTabBarHeight - 1
|
||||
: _kTabBarHeight,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -1172,22 +1225,26 @@ class _CloseButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: _kIconSize,
|
||||
child: Offstage(
|
||||
offstage: !visible,
|
||||
child: InkWell(
|
||||
hoverColor: MyTheme.tabbar(context).closeHoverColor,
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => onClose(),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: _kIconSize,
|
||||
color: tabSelected
|
||||
? MyTheme.tabbar(context).selectedIconColor
|
||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||
),
|
||||
),
|
||||
)).paddingOnly(left: 10);
|
||||
width: _kIconSize,
|
||||
child: () {
|
||||
if (visible) {
|
||||
return InkWell(
|
||||
hoverColor: MyTheme.tabbar(context).closeHoverColor,
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => onClose(),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: _kIconSize,
|
||||
color: tabSelected
|
||||
? MyTheme.tabbar(context).selectedIconColor
|
||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Offstage();
|
||||
}
|
||||
}())
|
||||
.paddingOnly(left: 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1216,13 +1273,7 @@ class ActionIcon extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ActionIconState extends State<ActionIcon> {
|
||||
var hover = false.obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hover.value = false;
|
||||
}
|
||||
final hover = false.obs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1341,27 +1392,30 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
||||
child: InkWell(child: Text(label)),
|
||||
),
|
||||
Obx(
|
||||
() => Offstage(
|
||||
offstage: !(tabInfo?.onTabCloseButton != null &&
|
||||
menuHover.value),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
tabInfo?.onTabCloseButton?.call();
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onHover: (event) =>
|
||||
setState(() => btnHover.value = true),
|
||||
onExit: (event) =>
|
||||
setState(() => btnHover.value = false),
|
||||
child: Icon(Icons.close,
|
||||
color:
|
||||
btnHover.value ? Colors.red : null))),
|
||||
),
|
||||
)
|
||||
() {
|
||||
if (tabInfo?.onTabCloseButton != null &&
|
||||
menuHover.value) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
tabInfo?.onTabCloseButton?.call();
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onHover: (event) =>
|
||||
setState(() => btnHover.value = true),
|
||||
onExit: (event) =>
|
||||
setState(() => btnHover.value = false),
|
||||
child: Icon(Icons.close,
|
||||
color:
|
||||
btnHover.value ? Colors.red : null)));
|
||||
} else {
|
||||
return Offstage();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1373,6 +1427,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _showTabBarBottomDivider(DesktopTabType tabType) {
|
||||
return tabType == DesktopTabType.main || tabType == DesktopTabType.install;
|
||||
}
|
||||
|
||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||
final Color? selectedTabIconColor;
|
||||
final Color? unSelectedTabIconColor;
|
||||
|
||||
@@ -260,7 +260,7 @@ showCmWindow({bool isStartup = false}) async {
|
||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
||||
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||
bind.mainHideDocker();
|
||||
bind.mainHideDock();
|
||||
await Future.wait([
|
||||
windowManager.show(),
|
||||
windowManager.focus(),
|
||||
@@ -288,14 +288,14 @@ hideCmWindow({bool isStartup = false}) async {
|
||||
size: kConnectionManagerWindowSizeClosedChat);
|
||||
windowManager.setOpacity(0);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||
bind.mainHideDocker();
|
||||
bind.mainHideDock();
|
||||
await windowManager.minimize();
|
||||
await windowManager.hide();
|
||||
_isCmReadyToShow = true;
|
||||
} else if (_isCmReadyToShow) {
|
||||
if (await windowManager.getOpacity() != 0) {
|
||||
await windowManager.setOpacity(0);
|
||||
bind.mainHideDocker();
|
||||
bind.mainHideDock();
|
||||
await windowManager.minimize();
|
||||
await windowManager.hide();
|
||||
}
|
||||
@@ -372,7 +372,7 @@ class App extends StatefulWidget {
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> {
|
||||
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -396,6 +396,34 @@ class _AppState extends State<App> {
|
||||
bind.mainChangeTheme(dark: to.toShortString());
|
||||
}
|
||||
};
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
_updateOrientation();
|
||||
}
|
||||
|
||||
void _updateOrientation() {
|
||||
if (isDesktop) return;
|
||||
|
||||
// Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
|
||||
// my test (Flutter 3.19.6, Android 14) is always the reverse value.
|
||||
// https://github.com/flutter/flutter/issues/60899
|
||||
// stateGlobal.isPortrait.value =
|
||||
// MediaQuery.of(context).orientation == Orientation.portrait;
|
||||
|
||||
final orientation = View.of(context).physicalSize.aspectRatio > 1
|
||||
? Orientation.landscape
|
||||
: Orientation.portrait;
|
||||
stateGlobal.isPortrait.value = orientation == Orientation.portrait;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,19 +9,16 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/autocomplete.dart';
|
||||
import '../../consts.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import 'home_page.dart';
|
||||
import 'scan_page.dart';
|
||||
import 'settings_page.dart';
|
||||
|
||||
/// Connection page for connecting to a remote peer.
|
||||
class ConnectionPage extends StatefulWidget implements PageShape {
|
||||
ConnectionPage({Key? key}) : super(key: key);
|
||||
ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
|
||||
|
||||
@override
|
||||
final icon = const Icon(Icons.connected_tv);
|
||||
@@ -30,7 +27,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
|
||||
final title = translate("Connection");
|
||||
|
||||
@override
|
||||
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
|
||||
final List<Widget> appBarActions;
|
||||
|
||||
@override
|
||||
State<ConnectionPage> createState() => _ConnectionPageState();
|
||||
@@ -50,19 +47,26 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
bool isPeersLoaded = false;
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
|
||||
_ConnectionPageState() {
|
||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||
_idController.addListener(() {
|
||||
_idEmpty.value = _idController.text.isEmpty;
|
||||
});
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
if (lastRemoteId != _idController.id) {
|
||||
setState(() {
|
||||
_idController.id = lastRemoteId;
|
||||
});
|
||||
}
|
||||
}();
|
||||
});
|
||||
}
|
||||
if (isAndroid) {
|
||||
if (!bind.isCustomClient()) {
|
||||
@@ -72,11 +76,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_idController.addListener(() {
|
||||
_idEmpty.value = _idController.text.isEmpty;
|
||||
});
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -250,6 +249,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
),
|
||||
),
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
onSubmitted: (_) {
|
||||
onConnect();
|
||||
},
|
||||
);
|
||||
},
|
||||
onSelected: (option) {
|
||||
@@ -354,73 +356,3 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class WebMenu extends StatefulWidget {
|
||||
const WebMenu({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<WebMenu> createState() => _WebMenuState();
|
||||
}
|
||||
|
||||
class _WebMenuState extends State<WebMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
return PopupMenuButton<String>(
|
||||
tooltip: "",
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
return (isIOS
|
||||
? [
|
||||
const PopupMenuItem(
|
||||
value: "scan",
|
||||
child: Icon(Icons.qr_code_scanner, color: Colors.black),
|
||||
)
|
||||
]
|
||||
: <PopupMenuItem<String>>[]) +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "server",
|
||||
child: Text(translate('ID/Relay Server')),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "login",
|
||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate("Login")
|
||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "about",
|
||||
child: Text('${translate('About')} RustDesk'),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 'server') {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'about') {
|
||||
showAbout(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'login') {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
}
|
||||
if (value == 'scan') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ScanPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,36 +204,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
setState(() {});
|
||||
} else if (v == "folder") {
|
||||
final name = TextEditingController();
|
||||
gFFI.dialogManager
|
||||
.show((setState, close, context) => CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate(
|
||||
"Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
String? errorText;
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
name.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
translate("Please enter the folder name"),
|
||||
errorText: errorText,
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel",
|
||||
onPressed: () => close(false),
|
||||
isOutline: true),
|
||||
dialogButton("OK", onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
currentFileController.createDir(
|
||||
PathUtil.join(
|
||||
currentDir.path,
|
||||
name.value.text,
|
||||
currentOptions.isWindows));
|
||||
close();
|
||||
}
|
||||
})
|
||||
]));
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel",
|
||||
onPressed: () => close(false), isOutline: true),
|
||||
dialogButton("OK", onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
if (!PathUtil.validName(
|
||||
name.value.text,
|
||||
currentFileController
|
||||
.options.value.isWindows)) {
|
||||
setState(() {
|
||||
errorText =
|
||||
translate("Invalid folder name");
|
||||
});
|
||||
return;
|
||||
}
|
||||
currentFileController.createDir(PathUtil.join(
|
||||
currentDir.path,
|
||||
name.value.text,
|
||||
currentOptions.isWindows));
|
||||
close();
|
||||
}
|
||||
})
|
||||
]);
|
||||
});
|
||||
} else if (v == "hidden") {
|
||||
currentFileController.toggleShowHidden();
|
||||
}
|
||||
@@ -497,7 +515,15 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
child: Text(translate("Properties")),
|
||||
value: "properties",
|
||||
enabled: false,
|
||||
)
|
||||
),
|
||||
if (!entries[index].isDrive &&
|
||||
versionCmp(gFFI.ffiModel.pi.version,
|
||||
"1.3.0") >=
|
||||
0)
|
||||
PopupMenuItem(
|
||||
child: Text(translate("Rename")),
|
||||
value: "rename",
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (v) {
|
||||
@@ -509,6 +535,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
_selectedItems.clear();
|
||||
widget.selectMode.toggle(isLocal);
|
||||
setState(() {});
|
||||
} else if (v == "rename") {
|
||||
controller.renameAction(
|
||||
entries[index], isLocal);
|
||||
}
|
||||
}),
|
||||
onTap: () {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/server_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:flutter_hbb/web/settings_page.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/chat_page.dart';
|
||||
@@ -45,7 +46,11 @@ class HomePageState extends State<HomePage> {
|
||||
|
||||
void initPages() {
|
||||
_pages.clear();
|
||||
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
|
||||
if (!bind.isIncomingOnly()) {
|
||||
_pages.add(ConnectionPage(
|
||||
appBarActions: [],
|
||||
));
|
||||
}
|
||||
if (isAndroid && !bind.isOutgoingOnly()) {
|
||||
_chatPageTabIndex = _pages.length;
|
||||
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
||||
@@ -149,7 +154,8 @@ class HomePageState extends State<HomePage> {
|
||||
}
|
||||
|
||||
class WebHomePage extends StatelessWidget {
|
||||
final connectionPage = ConnectionPage();
|
||||
final connectionPage =
|
||||
ConnectionPage(appBarActions: <Widget>[const WebSettingsPage()]);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -34,7 +34,7 @@ class RemotePage extends StatefulWidget {
|
||||
final bool? isSharedPassword;
|
||||
|
||||
@override
|
||||
State<RemotePage> createState() => _RemotePageState();
|
||||
State<RemotePage> createState() => _RemotePageState(id);
|
||||
}
|
||||
|
||||
class _RemotePageState extends State<RemotePage> {
|
||||
@@ -55,6 +55,15 @@ class _RemotePageState extends State<RemotePage> {
|
||||
InputModel get inputModel => gFFI.inputModel;
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -77,12 +86,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
keyboardSubscription =
|
||||
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
||||
initSharedStates(widget.id);
|
||||
gFFI.chatModel
|
||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
_blockableOverlayState.applyFfi(gFFI);
|
||||
gFFI.dialogManager.loadMobileActionsOverlayVisible();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -145,37 +151,59 @@ class _RemotePageState extends State<RemotePage> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// handle mobile virtual keyboard
|
||||
void handleSoftKeyboardInput(String newValue) {
|
||||
void _handleIOSSoftKeyboardInput(String newValue) {
|
||||
var oldValue = _value;
|
||||
_value = newValue;
|
||||
if (isIOS) {
|
||||
var i = newValue.length - 1;
|
||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
||||
var j = oldValue.length - 1;
|
||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
||||
if (i < j) j = i;
|
||||
newValue = newValue.substring(j + 1);
|
||||
oldValue = oldValue.substring(j + 1);
|
||||
var common = 0;
|
||||
for (;
|
||||
common < oldValue.length &&
|
||||
common < newValue.length &&
|
||||
newValue[common] == oldValue[common];
|
||||
++common) {}
|
||||
for (i = 0; i < oldValue.length - common; ++i) {
|
||||
inputModel.inputKey('VK_BACK');
|
||||
}
|
||||
if (newValue.length > common) {
|
||||
var s = newValue.substring(common);
|
||||
if (s.length > 1) {
|
||||
bind.sessionInputString(sessionId: sessionId, value: s);
|
||||
} else {
|
||||
inputChar(s);
|
||||
}
|
||||
}
|
||||
return;
|
||||
var i = newValue.length - 1;
|
||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
||||
var j = oldValue.length - 1;
|
||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
||||
if (i < j) j = i;
|
||||
var subNewValue = newValue.substring(j + 1);
|
||||
var subOldValue = oldValue.substring(j + 1);
|
||||
|
||||
// get common prefix of subNewValue and subOldValue
|
||||
var common = 0;
|
||||
for (;
|
||||
common < subOldValue.length &&
|
||||
common < subNewValue.length &&
|
||||
subNewValue[common] == subOldValue[common];
|
||||
++common) {}
|
||||
|
||||
// get newStr from subNewValue
|
||||
var newStr = "";
|
||||
if (subNewValue.length > common) {
|
||||
newStr = subNewValue.substring(common);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 &&
|
||||
newValue.isNotEmpty &&
|
||||
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) {
|
||||
if (char == '\n') {
|
||||
char = 'VK_RETURN';
|
||||
@@ -227,6 +264,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
// destroy first, so that our _value trick can work
|
||||
_value = initText;
|
||||
_textController.text = _value;
|
||||
setState(() => _showEdit = false);
|
||||
_timer?.cancel();
|
||||
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
||||
@@ -242,12 +280,10 @@ class _RemotePageState extends State<RemotePage> {
|
||||
});
|
||||
}
|
||||
|
||||
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
|
||||
|
||||
Widget _bottomWidget() => _showGestureHelp
|
||||
? getGestureHelp()
|
||||
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
|
||||
? getBottomAppBar(keyboard)
|
||||
? getBottomAppBar()
|
||||
: Offstage());
|
||||
|
||||
@override
|
||||
@@ -314,7 +350,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
return Container(
|
||||
color: kColorCanvas,
|
||||
child: isWebDesktop
|
||||
? getBodyForDesktopWithListener(keyboard)
|
||||
? getBodyForDesktopWithListener()
|
||||
: SafeArea(
|
||||
child:
|
||||
OrientationBuilder(builder: (ctx, orientation) {
|
||||
@@ -346,9 +382,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
}
|
||||
|
||||
Widget getRawPointerAndKeyBody(Widget child) {
|
||||
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
|
||||
final ffiModel = Provider.of<FfiModel>(context);
|
||||
return RawPointerMouseRegion(
|
||||
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
||||
cursor: ffiModel.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
||||
inputModel: inputModel,
|
||||
// Disable RawKeyFocusScope before the connecting is established.
|
||||
// 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(
|
||||
elevation: 10,
|
||||
color: MyTheme.accent,
|
||||
@@ -387,7 +424,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
},
|
||||
)
|
||||
] +
|
||||
(isWebDesktop
|
||||
(isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
|
||||
? []
|
||||
: gFFI.ffiModel.isPeerAndroid
|
||||
? [
|
||||
@@ -491,7 +528,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
autofocus: true,
|
||||
focusNode: _mobileFocusNode,
|
||||
maxLines: null,
|
||||
initialValue: _value,
|
||||
controller: _textController,
|
||||
// trick way to make backspace work always
|
||||
keyboardType: TextInputType.multiline,
|
||||
onChanged: handleSoftKeyboardInput,
|
||||
@@ -499,48 +536,84 @@ class _RemotePageState extends State<RemotePage> {
|
||||
),
|
||||
];
|
||||
if (showCursorPaint) {
|
||||
paints.add(CursorPaint());
|
||||
paints.add(CursorPaint(widget.id));
|
||||
}
|
||||
return paints;
|
||||
}()));
|
||||
}
|
||||
|
||||
Widget getBodyForDesktopWithListener(bool keyboard) {
|
||||
Widget getBodyForDesktopWithListener() {
|
||||
final ffiModel = Provider.of<FfiModel>(context);
|
||||
var paints = <Widget>[ImagePaint()];
|
||||
if (showCursorPaint) {
|
||||
final cursor = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
if (keyboard || cursor) {
|
||||
paints.add(CursorPaint());
|
||||
if (ffiModel.keyboard || cursor) {
|
||||
paints.add(CursorPaint(widget.id));
|
||||
}
|
||||
}
|
||||
return Container(
|
||||
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 {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final x = 120.0;
|
||||
final y = size.height;
|
||||
final mobileActionMenus = _getMobileActionMenus();
|
||||
final menus = toolbarControls(context, id, gFFI);
|
||||
getChild(TTextMenu menu) {
|
||||
if (menu.trailingIcon != null) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
menu.child,
|
||||
menu.trailingIcon!,
|
||||
]);
|
||||
} else {
|
||||
return menu.child;
|
||||
}
|
||||
}
|
||||
|
||||
final more = menus
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
||||
.toList();
|
||||
final List<PopupMenuEntry<int>> more = [
|
||||
...mobileActionMenus
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) =>
|
||||
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 {
|
||||
var index = await showMenu(
|
||||
context: context,
|
||||
@@ -548,8 +621,12 @@ class _RemotePageState extends State<RemotePage> {
|
||||
items: more,
|
||||
elevation: 8,
|
||||
);
|
||||
if (index != null && index < menus.length) {
|
||||
menus[index].onPressed.call();
|
||||
if (index != null) {
|
||||
if (index < mobileActionMenus.length) {
|
||||
mobileActionMenus[index].onPressed.call();
|
||||
} else if (index < mobileActionMenus.length + more.length) {
|
||||
menus[index - mobileActionMenus.length].onPressed.call();
|
||||
}
|
||||
}
|
||||
}();
|
||||
}
|
||||
@@ -569,9 +646,11 @@ class _RemotePageState extends State<RemotePage> {
|
||||
child: Text(translate(label), style: labelStyle),
|
||||
trailingIcon: Transform.scale(
|
||||
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||
child: IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
child: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: null,
|
||||
icon: icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
@@ -602,23 +681,11 @@ class _RemotePageState extends State<RemotePage> {
|
||||
),
|
||||
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
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
||||
.map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
|
||||
.toList();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
final size = MediaQuery.of(context).size;
|
||||
@@ -712,11 +779,6 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
onPressed: onPressed);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
_updateRect() {
|
||||
RenderObject? renderObject = _key.currentContext?.findRenderObject();
|
||||
if (renderObject == null) {
|
||||
@@ -885,26 +947,52 @@ class ImagePaint extends StatelessWidget {
|
||||
}
|
||||
|
||||
class CursorPaint extends StatelessWidget {
|
||||
late final String id;
|
||||
CursorPaint(this.id);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<CursorModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
final ffiModel = Provider.of<FfiModel>(context);
|
||||
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
||||
var s = c.scale;
|
||||
final s = c.scale;
|
||||
double hotx = m.hotx;
|
||||
double hoty = m.hoty;
|
||||
if (m.image == null) {
|
||||
var image = m.image;
|
||||
if (image == null) {
|
||||
if (preDefaultCursor.image != null) {
|
||||
image = preDefaultCursor.image;
|
||||
hotx = preDefaultCursor.image!.width / 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(
|
||||
painter: ImagePainter(
|
||||
image: m.image ?? preDefaultCursor.image,
|
||||
x: m.x * s - hotx + c.x,
|
||||
y: m.y * s - hoty + c.y - adjust,
|
||||
scale: 1),
|
||||
image: image,
|
||||
x: (m.x - hotx) * factor + c.x / s2,
|
||||
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
|
||||
scale: s2),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -982,22 +1070,40 @@ void showOptions(
|
||||
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
||||
final radios = [
|
||||
for (var e in viewStyleRadios)
|
||||
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) viewStyle.value = v;
|
||||
})),
|
||||
Obx(() => getRadio<String>(
|
||||
e.child,
|
||||
e.value,
|
||||
viewStyle.value,
|
||||
e.onChanged != null
|
||||
? (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) viewStyle.value = v;
|
||||
}
|
||||
: null)),
|
||||
const Divider(color: MyTheme.border),
|
||||
for (var e in imageQualityRadios)
|
||||
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) imageQuality.value = v;
|
||||
})),
|
||||
Obx(() => getRadio<String>(
|
||||
e.child,
|
||||
e.value,
|
||||
imageQuality.value,
|
||||
e.onChanged != null
|
||||
? (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) imageQuality.value = v;
|
||||
}
|
||||
: null)),
|
||||
const Divider(color: MyTheme.border),
|
||||
for (var e in codecRadios)
|
||||
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) codec.value = v;
|
||||
})),
|
||||
Obx(() => getRadio<String>(
|
||||
e.child,
|
||||
e.value,
|
||||
codec.value,
|
||||
e.onChanged != null
|
||||
? (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) codec.value = v;
|
||||
}
|
||||
: null)),
|
||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||
];
|
||||
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
||||
@@ -1008,10 +1114,12 @@ void showOptions(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxCursorToggleValues[e.key].value,
|
||||
onChanged: (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||
},
|
||||
onChanged: e.value.onChanged != null
|
||||
? (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||
}
|
||||
: null,
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
|
||||
@@ -1023,10 +1131,12 @@ void showOptions(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxToggleValues[e.key].value,
|
||||
onChanged: (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxToggleValues[e.key].value = v;
|
||||
},
|
||||
onChanged: e.value.onChanged != null
|
||||
? (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxToggleValues[e.key].value = v;
|
||||
}
|
||||
: null,
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
final toggles = [
|
||||
@@ -1046,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(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: displays + radios + toggles + [privacyModeWidget]),
|
||||
children: displays +
|
||||
radios +
|
||||
popupDialogMenus +
|
||||
toggles +
|
||||
[privacyModeWidget]),
|
||||
);
|
||||
}, 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) {
|
||||
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
||||
if (isMac) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.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/models/chat_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -22,7 +23,22 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
final icon = const Icon(Icons.mobile_screen_share);
|
||||
|
||||
@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>(
|
||||
tooltip: "",
|
||||
icon: const Icon(Icons.more_vert),
|
||||
@@ -101,18 +117,27 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
onSelected: (value) async {
|
||||
if (value == "changeID") {
|
||||
changeIdDialog();
|
||||
} else if (value == "setPermanentPassword") {
|
||||
setPermanentPasswordDialog(gFFI.dialogManager);
|
||||
setPasswordDialog();
|
||||
} else if (value == "setTemporaryPasswordLength") {
|
||||
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
||||
} else if (value == kUsePermanentPassword ||
|
||||
value == kUseTemporaryPassword ||
|
||||
value == kUseBothPasswords) {
|
||||
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
||||
gFFI.serverModel.updatePasswordModel();
|
||||
callback() {
|
||||
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
||||
gFFI.serverModel.updatePasswordModel();
|
||||
}
|
||||
|
||||
if (value == kUsePermanentPassword &&
|
||||
(await bind.mainGetPermanentPassword()).isEmpty) {
|
||||
setPasswordDialog(notEmptyCallback: callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
} else if (value.startsWith("AcceptSessionsVia")) {
|
||||
value = value.substring("AcceptSessionsVia".length);
|
||||
if (value == "Password") {
|
||||
@@ -126,10 +151,10 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
})
|
||||
];
|
||||
|
||||
ServerPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ServerPageState();
|
||||
Widget build(BuildContext context) {
|
||||
return actions[0];
|
||||
}
|
||||
}
|
||||
|
||||
class _ServerPageState extends State<ServerPage> {
|
||||
@@ -162,7 +187,7 @@ class _ServerPageState extends State<ServerPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
buildPresetPasswordWarning(),
|
||||
buildPresetPasswordWarningMobile(),
|
||||
gFFI.serverModel.isStart
|
||||
? ServerInfo()
|
||||
: ServiceNotRunningNotification(),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.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:provider/provider.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
@@ -83,18 +85,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _fingerprint = "";
|
||||
var _buildDate = "";
|
||||
var _autoDisconnectTimeout = "";
|
||||
var _hideServer = false;
|
||||
var _hideProxy = false;
|
||||
var _hideNetwork = false;
|
||||
var _enableTrustedDevices = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_SettingsState() {
|
||||
_enableAbr = option2bool(
|
||||
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
||||
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
||||
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
|
||||
_onlyWhiteList = (bind.mainGetOptionSync(key: kOptionWhitelist)) !=
|
||||
defaultOptionWhitelist;
|
||||
_onlyWhiteList = whitelistNotEmpty();
|
||||
_enableDirectIPAccess = option2bool(
|
||||
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
|
||||
_enableRecordSession = option2bool(kOptionEnableRecordSession,
|
||||
@@ -109,8 +110,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
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);
|
||||
}
|
||||
|
||||
() async {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
var update = false;
|
||||
|
||||
if (_hasIgnoreBattery) {
|
||||
@@ -169,7 +182,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
if (update) {
|
||||
setState(() {});
|
||||
}
|
||||
}();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -233,18 +246,76 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
],
|
||||
));
|
||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
final enable2fa = bind.mainHasValid2FaSync();
|
||||
final List<AbstractSettingsTile> tfaTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('enable-2fa-title')),
|
||||
initialValue: bind.mainHasValid2FaSync(),
|
||||
onToggle: (_) async {
|
||||
initialValue: enable2fa,
|
||||
onToggle: (v) async {
|
||||
update() async {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
change2fa(callback: update);
|
||||
if (v == false) {
|
||||
CommonConfirmDialog(
|
||||
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
||||
change2fa(callback: update);
|
||||
});
|
||||
} else {
|
||||
change2fa(callback: update);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (enable2fa)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Telegram bot')),
|
||||
initialValue: bind.mainHasValidBotSync(),
|
||||
onToggle: (v) async {
|
||||
update() async {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
if (v == false) {
|
||||
CommonConfirmDialog(
|
||||
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
|
||||
changeBot(callback: update);
|
||||
});
|
||||
} else {
|
||||
changeBot(callback: update);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (enable2fa)
|
||||
SettingsTile.switchTile(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate('Enable trusted devices')),
|
||||
Text('* ${translate('enable-trusted-devices-tip')}',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
initialValue: _enableTrustedDevices,
|
||||
onToggle: isOptionFixed(kOptionEnableTrustedDevices)
|
||||
? null
|
||||
: (v) async {
|
||||
mainSetBoolOption(kOptionEnableTrustedDevices, v);
|
||||
setState(() {
|
||||
_enableTrustedDevices = v;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (enable2fa && _enableTrustedDevices)
|
||||
SettingsTile(
|
||||
title: Text(translate('Manage trusted devices')),
|
||||
trailing: Icon(Icons.arrow_forward_ios),
|
||||
onPressed: (context) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return _ManageTrustedDevices();
|
||||
}));
|
||||
})
|
||||
];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Deny LAN discovery')),
|
||||
initialValue: _denyLANDiscovery,
|
||||
@@ -273,9 +344,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
initialValue: _onlyWhiteList,
|
||||
onToggle: (_) async {
|
||||
update() async {
|
||||
final onlyWhiteList =
|
||||
(await bind.mainGetOption(key: kOptionWhitelist)) !=
|
||||
defaultOptionWhitelist;
|
||||
final onlyWhiteList = whitelistNotEmpty();
|
||||
if (onlyWhiteList != _onlyWhiteList) {
|
||||
setState(() {
|
||||
_onlyWhiteList = onlyWhiteList;
|
||||
@@ -530,6 +599,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
));
|
||||
|
||||
final disabledSettings = bind.isDisableSettings();
|
||||
final hideSecuritySettings =
|
||||
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
|
||||
final settings = SettingsList(
|
||||
sections: [
|
||||
customClientSection,
|
||||
@@ -553,13 +624,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
],
|
||||
),
|
||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||
if (!disabledSettings)
|
||||
if (!disabledSettings && !_hideNetwork && !_hideServer)
|
||||
SettingsTile(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}),
|
||||
if (!isIOS && !_hideNetwork && !_hideProxy)
|
||||
SettingsTile(
|
||||
title: Text(translate('Socks5/Http(s) Proxy')),
|
||||
leading: Icon(Icons.network_ping),
|
||||
onPressed: (context) {
|
||||
changeSocks5Proxy();
|
||||
}),
|
||||
SettingsTile(
|
||||
title: Text(translate('Language')),
|
||||
leading: Icon(Icons.translate),
|
||||
@@ -625,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(
|
||||
title: Text(translate("Share Screen")),
|
||||
tiles: shareScreenTiles,
|
||||
),
|
||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
||||
if (isAndroid &&
|
||||
!disabledSettings &&
|
||||
!outgoingOnly &&
|
||||
!hideSecuritySettings)
|
||||
SettingsSection(
|
||||
title: Text(translate("Enhancements")),
|
||||
tiles: enhancementsTiles,
|
||||
@@ -786,7 +875,7 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
||||
void showAbout(OverlayDialogManager dialogManager) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text('${translate('About')} RustDesk'),
|
||||
title: Text(translate('About RustDesk')),
|
||||
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
||||
Text('Version: $version'),
|
||||
InkWell(
|
||||
@@ -940,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 {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@@ -41,18 +41,16 @@ class GestureHelp extends StatefulWidget {
|
||||
final OnTouchModeChange onTouchModeChange;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GestureHelpState();
|
||||
State<StatefulWidget> createState() => _GestureHelpState(touchMode);
|
||||
}
|
||||
|
||||
class _GestureHelpState extends State<GestureHelp> {
|
||||
var _selectedIndex;
|
||||
var _touchMode;
|
||||
late int _selectedIndex;
|
||||
late bool _touchMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_touchMode = widget.touchMode;
|
||||
_GestureHelpState(bool touchMode) {
|
||||
_touchMode = touchMode;
|
||||
_selectedIndex = _touchMode ? 1 : 0;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,17 +17,17 @@ import '../common.dart';
|
||||
|
||||
final syncAbOption = 'sync-ab-with-recent-sessions';
|
||||
bool shouldSyncAb() {
|
||||
return bind.mainGetLocalOption(key: syncAbOption).isNotEmpty;
|
||||
return bind.mainGetLocalOption(key: syncAbOption) == 'Y';
|
||||
}
|
||||
|
||||
final sortAbTagsOption = 'sync-ab-tags';
|
||||
bool shouldSortTags() {
|
||||
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
|
||||
return bind.mainGetLocalOption(key: sortAbTagsOption) == 'Y';
|
||||
}
|
||||
|
||||
final filterAbTagOption = 'filter-ab-by-intersection';
|
||||
bool filterAbTagByIntersection() {
|
||||
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty;
|
||||
return bind.mainGetLocalOption(key: filterAbTagOption) == 'Y';
|
||||
}
|
||||
|
||||
const _personalAddressBookName = "My address book";
|
||||
@@ -111,9 +111,10 @@ class AbModel {
|
||||
Future<void> _pullAb(
|
||||
{required ForcePullAb? force, required bool quiet}) async {
|
||||
if (bind.isDisableAb()) return;
|
||||
debugPrint("pullAb, force: $force, quiet: $quiet");
|
||||
if (!gFFI.userModel.isLogin) return;
|
||||
if (gFFI.userModel.networkError.isNotEmpty) return;
|
||||
if (force == null && listInitialized && current.initialized) return;
|
||||
debugPrint("pullAb, force: $force, quiet: $quiet");
|
||||
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
|
||||
try {
|
||||
// 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 {
|
||||
final sortTags = shouldSortTags().obs;
|
||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||
// licensedDevices is obtained from personal ab, shared ab restrict it in server
|
||||
var licensedDevices = 0;
|
||||
@@ -1209,8 +1208,6 @@ class LegacyAb extends BaseAb {
|
||||
class Ab extends BaseAb {
|
||||
AbProfile profile;
|
||||
late final bool personal;
|
||||
final sortTags = shouldSortTags().obs;
|
||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||
|
||||
Ab(this.profile, this.personal);
|
||||
|
||||
@@ -33,6 +33,8 @@ class CmFileModel {
|
||||
_onFileRemove(evt['remove']);
|
||||
} else if (evt['create_dir'] != null) {
|
||||
_onDirCreate(evt['create_dir']);
|
||||
} else if (evt['rename'] != null) {
|
||||
_onRename(evt['rename']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +61,6 @@ class CmFileModel {
|
||||
|
||||
_dealOneJob(dynamic l, bool calcSpeed) {
|
||||
final data = TransferJobSerdeData.fromJson(l);
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
@@ -70,12 +70,7 @@ class CmFileModel {
|
||||
if (job == null) {
|
||||
job = CmFileLog();
|
||||
jobTable.add(job);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == data.connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
_addUnread(data.connId);
|
||||
}
|
||||
job.id = data.id;
|
||||
job.action =
|
||||
@@ -167,8 +162,6 @@ class CmFileModel {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
FileActionLog data = FileActionLog.fromJson(d);
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
@@ -179,17 +172,45 @@ class CmFileModel {
|
||||
..fileName = data.path
|
||||
..action = CmFileAction.createDir
|
||||
..state = JobState.done);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == data.connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
_addUnread(data.connId);
|
||||
jobTable.refresh();
|
||||
} catch (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 {
|
||||
@@ -198,6 +219,7 @@ enum CmFileAction {
|
||||
localToRemote,
|
||||
remove,
|
||||
createDir,
|
||||
rename,
|
||||
}
|
||||
|
||||
class CmFileLog {
|
||||
@@ -285,3 +307,22 @@ class FileActionLog {
|
||||
dir: d['dir'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
class FileRenamenLog {
|
||||
int connId = 0;
|
||||
String path = '';
|
||||
String newName = '';
|
||||
|
||||
FileRenamenLog({
|
||||
required this.connId,
|
||||
required this.path,
|
||||
required this.newName,
|
||||
});
|
||||
|
||||
FileRenamenLog.fromJson(dynamic d)
|
||||
: this(
|
||||
connId: d['connId'] ?? 0,
|
||||
path: d['path'] ?? '',
|
||||
newName: d['newName'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@ class TextureModel {
|
||||
}
|
||||
|
||||
updateCurrentDisplay(int curDisplay) {
|
||||
if (isWeb) return;
|
||||
final ffi = parent.target;
|
||||
if (ffi == null) return;
|
||||
tryCreateTexture(int idx) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.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:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
@@ -33,6 +34,7 @@ class JobID {
|
||||
}
|
||||
|
||||
typedef GetSessionID = SessionID Function();
|
||||
typedef GetDialogManager = OverlayDialogManager? Function();
|
||||
|
||||
class FileModel {
|
||||
final WeakReference<FFI> parent;
|
||||
@@ -44,13 +46,15 @@ class FileModel {
|
||||
late final FileController remoteController;
|
||||
|
||||
late final GetSessionID getSessionID;
|
||||
late final GetDialogManager getDialogManager;
|
||||
SessionID get sessionId => getSessionID();
|
||||
late final FileDialogEventLoop evtLoop;
|
||||
|
||||
FileModel(this.parent) {
|
||||
getSessionID = () => parent.target!.sessionId;
|
||||
getDialogManager = () => parent.target?.dialogManager;
|
||||
fileFetcher = FileFetcher(getSessionID);
|
||||
jobController = JobController(getSessionID);
|
||||
jobController = JobController(getSessionID, getDialogManager);
|
||||
localController = FileController(
|
||||
isLocal: true,
|
||||
getSessionID: getSessionID,
|
||||
@@ -450,7 +454,7 @@ class FileController {
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
for (var from in items.items) {
|
||||
final jobID = jobController.add(from, isRemoteToLocal);
|
||||
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
|
||||
bind.sessionSendFiles(
|
||||
sessionId: sessionId,
|
||||
actId: jobID,
|
||||
@@ -493,13 +497,21 @@ class FileController {
|
||||
fd.format(isWindows);
|
||||
dialogManager?.dismissAll();
|
||||
if (fd.entries.isEmpty) {
|
||||
var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
|
||||
final confirm = await showRemoveDialog(
|
||||
translate(
|
||||
"Are you sure you want to delete this empty directory?"),
|
||||
item.name,
|
||||
false);
|
||||
if (confirm == true) {
|
||||
sendRemoveEmptyDir(item.path, 0);
|
||||
sendRemoveEmptyDir(
|
||||
item.path,
|
||||
0,
|
||||
deleteJobId,
|
||||
);
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
error: "cancel", state: JobState.done);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -507,6 +519,13 @@ class FileController {
|
||||
} else {
|
||||
entries = [];
|
||||
}
|
||||
int deleteJobId;
|
||||
if (item.isDirectory) {
|
||||
deleteJobId =
|
||||
jobController.addDeleteDirJob(item, !isLocal, entries.length);
|
||||
} else {
|
||||
deleteJobId = jobController.addDeleteFileJob(item, !isLocal);
|
||||
}
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
final dirShow = item.isDirectory
|
||||
@@ -521,24 +540,32 @@ class FileController {
|
||||
);
|
||||
try {
|
||||
if (confirm == true) {
|
||||
sendRemoveFile(entries[i].path, i);
|
||||
sendRemoveFile(entries[i].path, i, deleteJobId);
|
||||
final res = await jobController.jobResultListener.start();
|
||||
// handle remove res;
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i);
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
file_num: i, error: "cancel");
|
||||
}
|
||||
if (_removeCheckboxRemember) {
|
||||
if (confirm == true) {
|
||||
for (var j = i + 1; j < entries.length; j++) {
|
||||
sendRemoveFile(entries[j].path, j);
|
||||
sendRemoveFile(entries[j].path, j, deleteJobId);
|
||||
final res = await jobController.jobResultListener.start();
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i);
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
error: "cancel",
|
||||
file_num: entries.length,
|
||||
state: JobState.done);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -617,22 +644,19 @@ class FileController {
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
void sendRemoveFile(String path, int fileNum) {
|
||||
void sendRemoveFile(String path, int fileNum, int actId) {
|
||||
bind.sessionRemoveFile(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
actId: actId,
|
||||
path: path,
|
||||
isRemote: !isLocal,
|
||||
fileNum: fileNum);
|
||||
}
|
||||
|
||||
void sendRemoveEmptyDir(String path, int fileNum) {
|
||||
void sendRemoveEmptyDir(String path, int fileNum, int actId) {
|
||||
history.removeWhere((element) => element.contains(path));
|
||||
bind.sessionRemoveAllEmptyDirs(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
path: path,
|
||||
isRemote: !isLocal);
|
||||
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
|
||||
}
|
||||
|
||||
Future<void> createDir(String path) async {
|
||||
@@ -642,29 +666,102 @@ class FileController {
|
||||
path: path,
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const _kOneWayFileTransferError = 'one-way-file-transfer-tip';
|
||||
|
||||
class JobController {
|
||||
static final JobID jobID = JobID();
|
||||
final jobTable = List<JobProgress>.empty(growable: true).obs;
|
||||
final jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||
final GetSessionID getSessionID;
|
||||
final GetDialogManager getDialogManager;
|
||||
SessionID get sessionId => getSessionID();
|
||||
OverlayDialogManager? get alogManager => getDialogManager();
|
||||
int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
JobController(this.getSessionID);
|
||||
JobController(this.getSessionID, this.getDialogManager);
|
||||
|
||||
int getJob(int id) {
|
||||
return jobTable.indexWhere((element) => element.id == id);
|
||||
}
|
||||
|
||||
// JobProgress? getJob(int id) {
|
||||
// return jobTable.firstWhere((element) => element.id == id);
|
||||
// }
|
||||
|
||||
// return jobID
|
||||
int add(Entry from, bool isRemoteToLocal) {
|
||||
int addTransferJob(Entry from, bool isRemoteToLocal) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = path.basename(from.path)
|
||||
..jobName = from.path
|
||||
..totalSize = from.size
|
||||
@@ -674,6 +771,33 @@ class JobController {
|
||||
return jobID;
|
||||
}
|
||||
|
||||
int addDeleteFileJob(Entry file, bool isRemote) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.deleteFile
|
||||
..fileName = path.basename(file.path)
|
||||
..jobName = file.path
|
||||
..totalSize = file.size
|
||||
..state = JobState.none
|
||||
..id = jobID
|
||||
..isRemoteToLocal = isRemote);
|
||||
return jobID;
|
||||
}
|
||||
|
||||
int addDeleteDirJob(Entry file, bool isRemote, int fileCount) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.deleteDir
|
||||
..fileName = path.basename(file.path)
|
||||
..jobName = file.path
|
||||
..fileCount = fileCount
|
||||
..totalSize = file.size
|
||||
..state = JobState.none
|
||||
..id = jobID
|
||||
..isRemoteToLocal = isRemote);
|
||||
return jobID;
|
||||
}
|
||||
|
||||
void tryUpdateJobProgress(Map<String, dynamic> evt) {
|
||||
try {
|
||||
int id = int.parse(evt['id']);
|
||||
@@ -684,6 +808,7 @@ class JobController {
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
job.speed = double.parse(evt['speed']);
|
||||
job.finishedSize = int.parse(evt['finished_size']);
|
||||
job.recvJobRes = true;
|
||||
debugPrint("update job $id with $evt");
|
||||
jobTable.refresh();
|
||||
}
|
||||
@@ -692,20 +817,48 @@ class JobController {
|
||||
}
|
||||
}
|
||||
|
||||
void jobDone(Map<String, dynamic> evt) async {
|
||||
Future<bool> jobDone(Map<String, dynamic> evt) async {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
return;
|
||||
// return;
|
||||
}
|
||||
|
||||
int id = int.parse(evt['id']);
|
||||
int id = -1;
|
||||
int? fileNum = 0;
|
||||
double? speed = 0;
|
||||
try {
|
||||
id = int.parse(evt['id']);
|
||||
} catch (_) {}
|
||||
final jobIndex = getJob(id);
|
||||
if (jobIndex != -1) {
|
||||
final job = jobTable[jobIndex];
|
||||
job.finishedSize = job.totalSize;
|
||||
if (jobIndex == -1) return true;
|
||||
final job = jobTable[jobIndex];
|
||||
job.recvJobRes = true;
|
||||
if (job.type == JobType.deleteFile) {
|
||||
job.state = JobState.done;
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
jobTable.refresh();
|
||||
} else if (job.type == JobType.deleteDir) {
|
||||
try {
|
||||
fileNum = int.tryParse(evt['file_num']);
|
||||
} catch (_) {}
|
||||
if (fileNum != null) {
|
||||
if (fileNum < job.fileNum) return true; // file_num can be 0 at last
|
||||
job.fileNum = fileNum;
|
||||
if (fileNum >= job.fileCount - 1) {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fileNum = int.tryParse(evt['file_num']);
|
||||
speed = double.tryParse(evt['speed']);
|
||||
} catch (_) {}
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
if (speed != null) job.speed = speed;
|
||||
job.state = JobState.done;
|
||||
}
|
||||
jobTable.refresh();
|
||||
if (job.type == JobType.deleteDir) {
|
||||
return job.state == JobState.done;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,16 +869,61 @@ class JobController {
|
||||
final job = jobTable[jobIndex];
|
||||
job.state = JobState.error;
|
||||
job.err = err;
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
if (err == "skipped") {
|
||||
job.state = JobState.done;
|
||||
job.finishedSize = job.totalSize;
|
||||
job.recvJobRes = true;
|
||||
if (job.type == JobType.transfer) {
|
||||
int? fileNum = int.tryParse(evt['file_num']);
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
if (err == "skipped") {
|
||||
job.state = JobState.done;
|
||||
job.finishedSize = job.totalSize;
|
||||
}
|
||||
} else if (job.type == JobType.deleteDir) {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
}
|
||||
int? fileNum = int.tryParse(evt['file_num']);
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
} else if (job.type == JobType.deleteFile) {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
}
|
||||
}
|
||||
jobTable.refresh();
|
||||
}
|
||||
if (err == _kOneWayFileTransferError) {
|
||||
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
|
||||
final dm = alogManager;
|
||||
if (dm != null) {
|
||||
_lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
|
||||
msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm);
|
||||
}
|
||||
}
|
||||
}
|
||||
debugPrint("jobError $evt");
|
||||
}
|
||||
|
||||
void updateJobStatus(int id,
|
||||
{int? file_num, String? error, JobState? state}) {
|
||||
final jobIndex = getJob(id);
|
||||
if (jobIndex < 0) return;
|
||||
final job = jobTable[jobIndex];
|
||||
job.recvJobRes = true;
|
||||
if (file_num != null) {
|
||||
job.fileNum = file_num;
|
||||
}
|
||||
if (error != null) {
|
||||
job.err = error;
|
||||
job.state = JobState.error;
|
||||
}
|
||||
if (state != null) {
|
||||
job.state = state;
|
||||
}
|
||||
if (job.type == JobType.deleteFile && error == null) {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
jobTable.refresh();
|
||||
}
|
||||
|
||||
Future<void> cancelJob(int id) async {
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
}
|
||||
@@ -742,6 +940,7 @@ class JobController {
|
||||
final currJobId = JobController.jobID.next();
|
||||
String fileName = path.basename(isRemote ? remote : to);
|
||||
var jobProgress = JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = fileName
|
||||
..jobName = isRemote ? remote : to
|
||||
..id = currJobId
|
||||
@@ -1016,8 +1215,12 @@ extension JobStateDisplay on JobState {
|
||||
}
|
||||
}
|
||||
|
||||
enum JobType { none, transfer, deleteFile, deleteDir }
|
||||
|
||||
class JobProgress {
|
||||
JobType type = JobType.none;
|
||||
JobState state = JobState.none;
|
||||
var recvJobRes = false;
|
||||
var id = 0;
|
||||
var fileNum = 0;
|
||||
var speed = 0.0;
|
||||
@@ -1037,7 +1240,9 @@ class JobProgress {
|
||||
int lastTransferredSize = 0;
|
||||
|
||||
clear() {
|
||||
type = JobType.none;
|
||||
state = JobState.none;
|
||||
recvJobRes = false;
|
||||
id = 0;
|
||||
fileNum = 0;
|
||||
speed = 0;
|
||||
@@ -1051,11 +1256,81 @@ class JobProgress {
|
||||
}
|
||||
|
||||
String display() {
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
if (type == JobType.transfer) {
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
}
|
||||
} else if (type == JobType.deleteFile) {
|
||||
if (err == "cancel") {
|
||||
return translate("Cancel");
|
||||
}
|
||||
}
|
||||
|
||||
return state.display();
|
||||
}
|
||||
|
||||
String getStatus() {
|
||||
int handledFileCount = recvJobRes ? fileNum + 1 : fileNum;
|
||||
if (handledFileCount >= fileCount) {
|
||||
handledFileCount = fileCount;
|
||||
}
|
||||
if (state == JobState.done) {
|
||||
handledFileCount = fileCount;
|
||||
finishedSize = totalSize;
|
||||
}
|
||||
final filesStr = "$handledFileCount/$fileCount files";
|
||||
final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : "";
|
||||
final sizePercentStr = totalSize > 0 && finishedSize > 0
|
||||
? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}"
|
||||
: "";
|
||||
if (type == JobType.deleteFile) {
|
||||
return display();
|
||||
} else if (type == JobType.deleteDir) {
|
||||
var res = '';
|
||||
if (state == JobState.done || state == JobState.error) {
|
||||
res = display();
|
||||
}
|
||||
if (filesStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += " ";
|
||||
}
|
||||
res += filesStr;
|
||||
}
|
||||
|
||||
if (sizeStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizeStr;
|
||||
}
|
||||
return res;
|
||||
} else if (type == JobType.transfer) {
|
||||
var res = "";
|
||||
if (state != JobState.inProgress && state != JobState.none) {
|
||||
res += display();
|
||||
}
|
||||
if (filesStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += filesStr;
|
||||
}
|
||||
if (sizeStr.isNotEmpty && state != JobState.inProgress) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizeStr;
|
||||
}
|
||||
if (sizePercentStr.isNotEmpty && state == JobState.inProgress) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizePercentStr;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
class _PathStat {
|
||||
@@ -1083,6 +1358,13 @@ class PathUtil {
|
||||
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||
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 {
|
||||
|
||||
@@ -28,6 +28,7 @@ class GroupModel {
|
||||
Future<void> pull({force = true, quiet = false}) async {
|
||||
if (bind.isDisableGroupPanel()) return;
|
||||
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
||||
if (gFFI.userModel.networkError.isNotEmpty) return;
|
||||
if (!force && initialized) return;
|
||||
if (!quiet) {
|
||||
groupLoading.value = true;
|
||||
|
||||
@@ -177,7 +177,7 @@ class PointerEventToRust {
|
||||
}
|
||||
}
|
||||
|
||||
class ToReleaseKeys {
|
||||
class ToReleaseRawKeys {
|
||||
RawKeyEvent? lastLShiftKeyEvent;
|
||||
RawKeyEvent? lastRShiftKeyEvent;
|
||||
RawKeyEvent? lastLCtrlKeyEvent;
|
||||
@@ -282,6 +282,48 @@ class ToReleaseKeys {
|
||||
}
|
||||
}
|
||||
|
||||
class ToReleaseKeys {
|
||||
KeyEvent? lastLShiftKeyEvent;
|
||||
KeyEvent? lastRShiftKeyEvent;
|
||||
KeyEvent? lastLCtrlKeyEvent;
|
||||
KeyEvent? lastRCtrlKeyEvent;
|
||||
KeyEvent? lastLAltKeyEvent;
|
||||
KeyEvent? lastRAltKeyEvent;
|
||||
KeyEvent? lastLCommandKeyEvent;
|
||||
KeyEvent? lastRCommandKeyEvent;
|
||||
KeyEvent? lastSuperKeyEvent;
|
||||
|
||||
reset() {
|
||||
lastLShiftKeyEvent = null;
|
||||
lastRShiftKeyEvent = null;
|
||||
lastLCtrlKeyEvent = null;
|
||||
lastRCtrlKeyEvent = null;
|
||||
lastLAltKeyEvent = null;
|
||||
lastRAltKeyEvent = null;
|
||||
lastLCommandKeyEvent = null;
|
||||
lastRCommandKeyEvent = null;
|
||||
lastSuperKeyEvent = null;
|
||||
}
|
||||
|
||||
release(KeyEventResult Function(KeyEvent e) handleKeyEvent) {
|
||||
for (final key in [
|
||||
lastLShiftKeyEvent,
|
||||
lastRShiftKeyEvent,
|
||||
lastLCtrlKeyEvent,
|
||||
lastRCtrlKeyEvent,
|
||||
lastLAltKeyEvent,
|
||||
lastRAltKeyEvent,
|
||||
lastLCommandKeyEvent,
|
||||
lastRCommandKeyEvent,
|
||||
lastSuperKeyEvent,
|
||||
]) {
|
||||
if (key != null) {
|
||||
handleKeyEvent(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InputModel {
|
||||
final WeakReference<FFI> parent;
|
||||
String keyboardMode = '';
|
||||
@@ -292,6 +334,7 @@ class InputModel {
|
||||
var alt = false;
|
||||
var command = false;
|
||||
|
||||
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
|
||||
final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
|
||||
|
||||
// trackpad
|
||||
@@ -339,6 +382,91 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
void handleKeyDownEventModifiers(KeyEvent e) {
|
||||
KeyUpEvent upEvent(e) => KeyUpEvent(
|
||||
physicalKey: e.physicalKey,
|
||||
logicalKey: e.logicalKey,
|
||||
timeStamp: e.timeStamp,
|
||||
);
|
||||
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
|
||||
if (!alt) {
|
||||
alt = true;
|
||||
}
|
||||
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
|
||||
if (!alt) {
|
||||
alt = true;
|
||||
}
|
||||
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
|
||||
if (!ctrl) {
|
||||
ctrl = true;
|
||||
}
|
||||
toReleaseKeys.lastLCtrlKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
|
||||
if (!ctrl) {
|
||||
ctrl = true;
|
||||
}
|
||||
toReleaseKeys.lastRCtrlKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
|
||||
if (!shift) {
|
||||
shift = true;
|
||||
}
|
||||
toReleaseKeys.lastLShiftKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
|
||||
if (!shift) {
|
||||
shift = true;
|
||||
}
|
||||
toReleaseKeys.lastRShiftKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
|
||||
if (!command) {
|
||||
command = true;
|
||||
}
|
||||
toReleaseKeys.lastLCommandKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
|
||||
if (!command) {
|
||||
command = true;
|
||||
}
|
||||
toReleaseKeys.lastRCommandKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
|
||||
if (!command) {
|
||||
command = true;
|
||||
}
|
||||
toReleaseKeys.lastSuperKeyEvent = upEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void handleKeyUpEventModifiers(KeyEvent e) {
|
||||
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
|
||||
alt = false;
|
||||
toReleaseKeys.lastLAltKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
|
||||
alt = false;
|
||||
toReleaseKeys.lastRAltKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
|
||||
ctrl = false;
|
||||
toReleaseKeys.lastLCtrlKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
|
||||
ctrl = false;
|
||||
toReleaseKeys.lastRCtrlKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
|
||||
shift = false;
|
||||
toReleaseKeys.lastLShiftKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
|
||||
shift = false;
|
||||
toReleaseKeys.lastRShiftKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
|
||||
command = false;
|
||||
toReleaseKeys.lastLCommandKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
|
||||
command = false;
|
||||
toReleaseKeys.lastRCommandKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
|
||||
command = false;
|
||||
toReleaseKeys.lastSuperKeyEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
||||
@@ -358,7 +486,7 @@ class InputModel {
|
||||
command = true;
|
||||
}
|
||||
}
|
||||
toReleaseKeys.updateKeyDown(key, e);
|
||||
toReleaseRawKeys.updateKeyDown(key, e);
|
||||
}
|
||||
if (e is RawKeyUpEvent) {
|
||||
if (key == LogicalKeyboardKey.altLeft ||
|
||||
@@ -376,12 +504,46 @@ class InputModel {
|
||||
command = false;
|
||||
}
|
||||
|
||||
toReleaseKeys.updateKeyUp(key, e);
|
||||
toReleaseRawKeys.updateKeyUp(key, e);
|
||||
}
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == 'map') {
|
||||
mapKeyboardMode(e);
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
mapKeyboardModeRaw(e);
|
||||
} else {
|
||||
legacyKeyboardModeRaw(e);
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
KeyEventResult handleKeyEvent(KeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (isWindows || isLinux) {
|
||||
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
|
||||
if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||
|
||||
e.physicalKey == PhysicalKeyboardKey.metaRight) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
if (e is KeyUpEvent) {
|
||||
handleKeyUpEventModifiers(e);
|
||||
} else if (e is KeyDownEvent) {
|
||||
handleKeyDownEventModifiers(e);
|
||||
}
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
// FIXME: e.character is wrong for dead keys, eg: ^ in de
|
||||
newKeyboardMode(
|
||||
e.character ?? '',
|
||||
e.physicalKey.usbHidUsage & 0xFFFF,
|
||||
// Show repeat event be converted to "release+press" events?
|
||||
e is KeyDownEvent || e is KeyRepeatEvent);
|
||||
} else {
|
||||
legacyKeyboardMode(e);
|
||||
}
|
||||
@@ -389,7 +551,33 @@ class InputModel {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
void mapKeyboardMode(RawKeyEvent e) {
|
||||
/// Send Key Event
|
||||
void newKeyboardMode(String character, int usbHid, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
sessionId: sessionId,
|
||||
character: character,
|
||||
usbHid: usbHid,
|
||||
lockModes: lockModes,
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
void mapKeyboardModeRaw(RawKeyEvent e) {
|
||||
int positionCode = -1;
|
||||
int platformCode = -1;
|
||||
bool down;
|
||||
@@ -441,7 +629,7 @@ class InputModel {
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
bind.sessionHandleFlutterRawKeyEvent(
|
||||
sessionId: sessionId,
|
||||
name: name,
|
||||
platformCode: platformCode,
|
||||
@@ -450,7 +638,7 @@ class InputModel {
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
void legacyKeyboardMode(RawKeyEvent e) {
|
||||
void legacyKeyboardModeRaw(RawKeyEvent e) {
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (e.repeat) {
|
||||
sendRawKey(e, press: true);
|
||||
@@ -471,6 +659,24 @@ class InputModel {
|
||||
inputKey(label, down: down, press: press ?? false);
|
||||
}
|
||||
|
||||
void legacyKeyboardMode(KeyEvent e) {
|
||||
if (e is KeyDownEvent) {
|
||||
sendKey(e, down: true);
|
||||
} else if (e is KeyRepeatEvent) {
|
||||
sendKey(e, press: true);
|
||||
} else if (e is KeyUpEvent) {
|
||||
sendKey(e);
|
||||
}
|
||||
}
|
||||
|
||||
void sendKey(KeyEvent e, {bool? down, bool? press}) {
|
||||
// for maximum compatibility
|
||||
final label = physicalKeyMap[e.physicalKey.usbHidUsage] ??
|
||||
logicalKeyMap[e.logicalKey.keyId] ??
|
||||
e.logicalKey.keyLabel;
|
||||
inputKey(label, down: down, press: press ?? false);
|
||||
}
|
||||
|
||||
/// Send key stroke event.
|
||||
/// [down] indicates the key's state(down or up).
|
||||
/// [press] indicates a click event(down and up).
|
||||
@@ -566,7 +772,8 @@ class InputModel {
|
||||
}
|
||||
|
||||
void enterOrLeave(bool enter) {
|
||||
toReleaseKeys.release(handleRawKeyEvent);
|
||||
toReleaseKeys.release(handleKeyEvent);
|
||||
toReleaseRawKeys.release(handleRawKeyEvent);
|
||||
_pointerMovedAfterEnter = false;
|
||||
|
||||
// Fix status
|
||||
@@ -577,6 +784,9 @@ class InputModel {
|
||||
if (!isInputSourceFlutter) {
|
||||
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
|
||||
}
|
||||
if (!isWeb && enter) {
|
||||
bind.setCurSessionId(sessionId: sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send mouse movement event with distance in [x] and [y].
|
||||
@@ -1152,4 +1362,27 @@ class InputModel {
|
||||
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 {
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
|
||||
}
|
||||
|
||||
Future<void> onMobileVolumeUp() async =>
|
||||
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF);
|
||||
Future<void> onMobileVolumeDown() async =>
|
||||
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF);
|
||||
Future<void> onMobilePower() async =>
|
||||
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF);
|
||||
}
|
||||
|
||||
@@ -192,10 +192,10 @@ class FfiModel with ChangeNotifier {
|
||||
_permissions[k] = v == 'true';
|
||||
});
|
||||
// Only inited at remote page
|
||||
if (desktopType == DesktopType.remote) {
|
||||
if (parent.target?.connType == ConnType.defaultConn) {
|
||||
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||
}
|
||||
debugPrint('$_permissions');
|
||||
debugPrint('updatePermission: $_permissions');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -304,8 +304,13 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'job_progress') {
|
||||
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
|
||||
} else if (name == 'job_done') {
|
||||
parent.target?.fileModel.jobController.jobDone(evt);
|
||||
parent.target?.fileModel.refreshAll();
|
||||
bool? refresh =
|
||||
await parent.target?.fileModel.jobController.jobDone(evt);
|
||||
if (refresh == true) {
|
||||
// many job done for delete directory
|
||||
// todo: refresh may not work when confirm delete local directory
|
||||
parent.target?.fileModel.refreshAll();
|
||||
}
|
||||
} else if (name == 'job_error') {
|
||||
parent.target?.fileModel.jobController.jobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
@@ -384,7 +389,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'use_texture_render') {
|
||||
_handleUseTextureRender(evt, sessionId, peerId);
|
||||
} else {
|
||||
debugPrint('Unknown event name: $name');
|
||||
debugPrint('Event is not handled in the fixed branch: $name');
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -438,20 +443,6 @@ class FfiModel with ChangeNotifier {
|
||||
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
|
||||
final running = evt['running'] == 'true';
|
||||
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) {
|
||||
@@ -506,10 +497,12 @@ class FfiModel with ChangeNotifier {
|
||||
newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
|
||||
newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
|
||||
newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
|
||||
newDisplay.originalWidth =
|
||||
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
|
||||
newDisplay.originalHeight =
|
||||
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
|
||||
newDisplay.originalWidth = int.tryParse(
|
||||
evt['original_width'] ?? kInvalidResolutionValue.toString()) ??
|
||||
kInvalidResolutionValue;
|
||||
newDisplay.originalHeight = int.tryParse(
|
||||
evt['original_height'] ?? kInvalidResolutionValue.toString()) ??
|
||||
kInvalidResolutionValue;
|
||||
newDisplay._scale = _pi.scaleOfDisplay(display);
|
||||
_pi.displays[display] = newDisplay;
|
||||
|
||||
@@ -737,6 +730,8 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
/// Handle the peer info event based on [evt].
|
||||
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.
|
||||
parent.target?.inputModel.updateKeyboardMode();
|
||||
|
||||
@@ -800,7 +795,7 @@ class FfiModel with ChangeNotifier {
|
||||
isRefreshing = false;
|
||||
}
|
||||
Map<String, dynamic> features = json.decode(evt['features']);
|
||||
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
||||
_pi.features.privacyMode = features['privacy_mode'] == true;
|
||||
if (!isCache) {
|
||||
handleResolutions(peerId, evt["resolutions"]);
|
||||
}
|
||||
@@ -1015,14 +1010,15 @@ class FfiModel with ChangeNotifier {
|
||||
// Notify to switch display
|
||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
||||
'display_is_plugged_out_msg', '', parent.target!.dialogManager);
|
||||
final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex
|
||||
? 0
|
||||
: pi.primaryDisplay;
|
||||
final displays = newDisplay;
|
||||
final isPeerPrimaryDisplayValid =
|
||||
pi.primaryDisplay == kInvalidDisplayIndex ||
|
||||
pi.primaryDisplay >= pi.displays.length;
|
||||
final newDisplay =
|
||||
isPeerPrimaryDisplayValid ? 0 : pi.primaryDisplay;
|
||||
bind.sessionSwitchDisplay(
|
||||
isDesktop: isDesktop,
|
||||
sessionId: sessionId,
|
||||
value: Int32List.fromList([displays]),
|
||||
value: Int32List.fromList([newDisplay]),
|
||||
);
|
||||
|
||||
if (_pi.isSupportMultiUiSession) {
|
||||
@@ -1187,26 +1183,28 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||
|
||||
onRgba(int display, Uint8List rgba) {
|
||||
clearImage() => _image = null;
|
||||
|
||||
onRgba(int display, Uint8List rgba) async {
|
||||
try {
|
||||
await decodeAndUpdate(display, rgba);
|
||||
} catch (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);
|
||||
img.decodeImageFromPixels(
|
||||
rgba,
|
||||
rect?.width.toInt() ?? 0,
|
||||
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 {
|
||||
// my throw exception, because the listener maybe already dispose
|
||||
update(image);
|
||||
} catch (e) {
|
||||
debugPrint('update image: $e');
|
||||
}
|
||||
});
|
||||
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 {
|
||||
@@ -1740,7 +1738,7 @@ class PredefinedCursor {
|
||||
_image2 = img2.decodePng(base64Decode(png));
|
||||
if (_image2 != null) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -2187,6 +2185,7 @@ class CursorModel with ChangeNotifier {
|
||||
debugPrint("deleting cursor with key $k");
|
||||
deleteCustomCursor(k);
|
||||
}
|
||||
resetSystemCursor();
|
||||
}
|
||||
|
||||
trySetRemoteWindowCoords() {
|
||||
@@ -2233,8 +2232,10 @@ class QualityMonitorModel with ChangeNotifier {
|
||||
|
||||
updateQualityStatus(Map<String, dynamic> evt) {
|
||||
try {
|
||||
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed'];
|
||||
if ((evt['fps'] as String).isNotEmpty) {
|
||||
if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) {
|
||||
_data.speed = evt['speed'];
|
||||
}
|
||||
if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) {
|
||||
final fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi != null) {
|
||||
@@ -2255,14 +2256,18 @@ class QualityMonitorModel with ChangeNotifier {
|
||||
_data.fps = null;
|
||||
}
|
||||
}
|
||||
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay'];
|
||||
if ((evt['target_bitrate'] as String).isNotEmpty) {
|
||||
if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) {
|
||||
_data.delay = evt['delay'];
|
||||
}
|
||||
if (evt.containsKey('target_bitrate') &&
|
||||
(evt['target_bitrate'] as String).isNotEmpty) {
|
||||
_data.targetBitrate = evt['target_bitrate'];
|
||||
}
|
||||
if ((evt['codec_format'] as String).isNotEmpty) {
|
||||
if (evt.containsKey('codec_format') &&
|
||||
(evt['codec_format'] as String).isNotEmpty) {
|
||||
_data.codecFormat = evt['codec_format'];
|
||||
}
|
||||
if ((evt['chroma'] as String).isNotEmpty) {
|
||||
if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) {
|
||||
_data.chroma = evt['chroma'];
|
||||
}
|
||||
notifyListeners();
|
||||
@@ -2506,6 +2511,7 @@ class FFI {
|
||||
onEvent2UIRgba();
|
||||
imageModel.onRgba(display, data);
|
||||
});
|
||||
this.id = id;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2561,32 +2567,30 @@ class FFI {
|
||||
}
|
||||
} else if (message is EventToUI_Rgba) {
|
||||
final display = message.field0;
|
||||
if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) {
|
||||
//debugPrint("EventToUI_Rgba display:$display");
|
||||
textureModel.setTextureType(display: display, gpuTexture: false);
|
||||
// Fetch the image buffer from rust codes.
|
||||
final sz = platformFFI.getRgbaSize(sessionId, display);
|
||||
if (sz == 0) {
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
return;
|
||||
}
|
||||
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
||||
if (rgba != null) {
|
||||
onEvent2UIRgba();
|
||||
await imageModel.onRgba(display, rgba);
|
||||
} else {
|
||||
// Fetch the image buffer from rust codes.
|
||||
final sz = platformFFI.getRgbaSize(sessionId, display);
|
||||
if (sz == 0) {
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
return;
|
||||
}
|
||||
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
||||
if (rgba != null) {
|
||||
onEvent2UIRgba();
|
||||
imageModel.onRgba(display, rgba);
|
||||
} else {
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
}
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
}
|
||||
} else if (message is EventToUI_Texture) {
|
||||
final display = message.field0;
|
||||
debugPrint("EventToUI_Texture display:$display");
|
||||
if (hasGpuTextureRender) {
|
||||
textureModel.setTextureType(display: display, gpuTexture: true);
|
||||
onEvent2UIRgba();
|
||||
final gpuTexture = message.field1;
|
||||
debugPrint(
|
||||
"EventToUI_Texture display:$display, gpuTexture:$gpuTexture");
|
||||
if (gpuTexture && !hasGpuTextureRender) {
|
||||
debugPrint('the gpuTexture is not supported.');
|
||||
return;
|
||||
}
|
||||
textureModel.setTextureType(display: display, gpuTexture: gpuTexture);
|
||||
onEvent2UIRgba();
|
||||
}
|
||||
}();
|
||||
});
|
||||
@@ -2622,8 +2626,9 @@ class FFI {
|
||||
remember: remember);
|
||||
}
|
||||
|
||||
void send2FA(SessionID sessionId, String code) {
|
||||
bind.sessionSend2Fa(sessionId: sessionId, code: code);
|
||||
void send2FA(SessionID sessionId, String code, bool trustThisDevice) {
|
||||
bind.sessionSend2Fa(
|
||||
sessionId: sessionId, code: code, trustThisDevice: trustThisDevice);
|
||||
}
|
||||
|
||||
/// Close the remote session.
|
||||
@@ -2640,7 +2645,7 @@ class FFI {
|
||||
canvasModel.scale,
|
||||
ffiModel.pi.currentDisplay);
|
||||
}
|
||||
imageModel.update(null);
|
||||
await imageModel.update(null);
|
||||
cursorModel.clear();
|
||||
ffiModel.clear();
|
||||
canvasModel.clear();
|
||||
|
||||
@@ -117,9 +117,13 @@ class PlatformFFI {
|
||||
? DynamicLibrary.open('librustdesk.so')
|
||||
: isWindows
|
||||
? DynamicLibrary.open('librustdesk.dll')
|
||||
: isMacOS
|
||||
? DynamicLibrary.open("liblibrustdesk.dylib")
|
||||
: DynamicLibrary.process();
|
||||
:
|
||||
// Use executable itself as the dynamic library for MacOS.
|
||||
// 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');
|
||||
try {
|
||||
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
|
||||
|
||||
@@ -194,10 +194,14 @@ class Peers extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _updateOnlineState(Map<String, dynamic> evt) {
|
||||
int changedCount = 0;
|
||||
evt['onlines'].split(',').forEach((online) {
|
||||
for (var i = 0; i < peers.length; i++) {
|
||||
if (peers[i].id == online) {
|
||||
peers[i].online = true;
|
||||
if (!peers[i].online) {
|
||||
changedCount += 1;
|
||||
peers[i].online = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -205,13 +209,18 @@ class Peers extends ChangeNotifier {
|
||||
evt['offlines'].split(',').forEach((offline) {
|
||||
for (var i = 0; i < peers.length; i++) {
|
||||
if (peers[i].id == offline) {
|
||||
peers[i].online = false;
|
||||
if (peers[i].online) {
|
||||
changedCount += 1;
|
||||
peers[i].online = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
event = UpdateEvent.online;
|
||||
notifyListeners();
|
||||
if (changedCount > 0) {
|
||||
event = UpdateEvent.online;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _updatePeers(Map<String, dynamic> evt) {
|
||||
|
||||
@@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// `notifyListeners()` will cause many rebuilds.
|
||||
// So, we need to reduce the calls to "notifyListeners()" only when necessary.
|
||||
// A better way is to use a new model.
|
||||
setCurrentTabCachedPeers(List<Peer> peers) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
final isPreEmpty = _currentTabCachedPeers.isEmpty;
|
||||
_currentTabCachedPeers = peers;
|
||||
notifyListeners();
|
||||
final isNowEmpty = _currentTabCachedPeers.isEmpty;
|
||||
if (isPreEmpty != isNowEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,11 @@ class ServerModel with ChangeNotifier {
|
||||
await timerCallback();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial keyboard status is off on mobile
|
||||
if (isMobile) {
|
||||
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
|
||||
}
|
||||
}
|
||||
|
||||
/// 1. check android permission
|
||||
@@ -191,7 +196,7 @@ class ServerModel with ChangeNotifier {
|
||||
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
|
||||
} else {
|
||||
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
|
||||
_audioOk = audioOption.isEmpty;
|
||||
_audioOk = audioOption != 'N';
|
||||
}
|
||||
|
||||
// file
|
||||
@@ -201,7 +206,7 @@ class ServerModel with ChangeNotifier {
|
||||
} else {
|
||||
final fileOption =
|
||||
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||
_fileOk = fileOption.isEmpty;
|
||||
_fileOk = fileOption != 'N';
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
@@ -821,7 +826,7 @@ class Client {
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['is_start'] = authorized;
|
||||
data['authorized'] = authorized;
|
||||
data['is_file_transfer'] = isFileTransfer;
|
||||
data['port_forward'] = portForward;
|
||||
data['name'] = name;
|
||||
@@ -835,6 +840,8 @@ class Client {
|
||||
data['block_input'] = blockInput;
|
||||
data['disconnected'] = disconnected;
|
||||
data['from_switch'] = fromSwitch;
|
||||
data['in_voice_call'] = inVoiceCall;
|
||||
data['incoming_voice_call'] = incomingVoiceCall;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,14 @@ class StateGlobal {
|
||||
bool _isMinimized = false;
|
||||
final RxBool isMaximized = false.obs;
|
||||
final RxBool _showTabBar = true.obs;
|
||||
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize);
|
||||
final RxDouble _resizeEdgeSize = RxDouble(windowResizeEdgeSize);
|
||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||
final RxBool showRemoteToolBar = false.obs;
|
||||
final svcStatus = SvcStatus.notReady.obs;
|
||||
final RxBool isFocused = false.obs;
|
||||
|
||||
final isPortrait = false.obs;
|
||||
|
||||
String _inputSource = '';
|
||||
|
||||
// Use for desktop -> remote toolbar -> resolution
|
||||
@@ -93,7 +95,7 @@ class StateGlobal {
|
||||
? kFullScreenEdgeSize
|
||||
: isMaximized.isTrue
|
||||
? kMaximizeEdgeSize
|
||||
: windowEdgeSize;
|
||||
: windowResizeEdgeSize;
|
||||
|
||||
String getInputSource({bool force = false}) {
|
||||
if (force || _inputSource.isEmpty) {
|
||||
|
||||
@@ -17,13 +17,23 @@ bool refreshingUser = false;
|
||||
class UserModel {
|
||||
final RxString userName = ''.obs;
|
||||
final RxBool isAdmin = false.obs;
|
||||
final RxString networkError = ''.obs;
|
||||
bool get isLogin => userName.isNotEmpty;
|
||||
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 {
|
||||
if (bind.isDisableAccount()) return;
|
||||
networkError.value = '';
|
||||
final token = bind.mainGetLocalOption(key: 'access_token');
|
||||
if (token == '') {
|
||||
await updateOtherModels();
|
||||
@@ -38,12 +48,18 @@ class UserModel {
|
||||
if (refreshingUser) return;
|
||||
try {
|
||||
refreshingUser = true;
|
||||
final response = await http.post(Uri.parse('$url/api/currentUser'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token'
|
||||
},
|
||||
body: json.encode(body));
|
||||
final http.Response response;
|
||||
try {
|
||||
response = await http.post(Uri.parse('$url/api/currentUser'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token'
|
||||
},
|
||||
body: json.encode(body));
|
||||
} catch (e) {
|
||||
networkError.value = e.toString();
|
||||
rethrow;
|
||||
}
|
||||
refreshingUser = false;
|
||||
final status = response.statusCode;
|
||||
if (status == 401 || status == 400) {
|
||||
|
||||
@@ -11,3 +11,7 @@ final isWebDesktop_ = false;
|
||||
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
|
||||
String get screenInfo_ => '';
|
||||
|
||||
final isWebOnWindows_ = false;
|
||||
final isWebOnLinux_ = false;
|
||||
final isWebOnMacOS_ = false;
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/model.dart';
|
||||
|
||||
deleteCustomCursor(String key) =>
|
||||
custom_cursor_manager.CursorManager.instance.deleteCursor(key);
|
||||
resetSystemCursor() {}
|
||||
|
||||
MouseCursor buildCursorOfCache(
|
||||
CursorModel cursor, double scale, CursorData? cache) {
|
||||
|
||||
@@ -13,14 +13,12 @@ Future<ui.Image?> decodeImageFromPixels(
|
||||
int? rowBytes,
|
||||
int? targetWidth,
|
||||
int? targetHeight,
|
||||
VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds
|
||||
bool allowUpscaling = true,
|
||||
}) async {
|
||||
if (targetWidth != null) {
|
||||
assert(allowUpscaling || targetWidth <= width);
|
||||
if (!(allowUpscaling || targetWidth <= width)) {
|
||||
print("not allow upscaling but targetWidth > width");
|
||||
onPixelsCopied?.call();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +26,6 @@ Future<ui.Image?> decodeImageFromPixels(
|
||||
assert(allowUpscaling || targetHeight <= height);
|
||||
if (!(allowUpscaling || targetHeight <= height)) {
|
||||
print("not allow upscaling but targetHeight > height");
|
||||
onPixelsCopied?.call();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -36,9 +33,7 @@ Future<ui.Image?> decodeImageFromPixels(
|
||||
final ui.ImmutableBuffer buffer;
|
||||
try {
|
||||
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||
onPixelsCopied?.call();
|
||||
} catch (e) {
|
||||
onPixelsCopied?.call();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'dart:html' as html;
|
||||
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
|
||||
@@ -23,6 +24,7 @@ sealed class EventToUI {
|
||||
) = EventToUI_Rgba;
|
||||
const factory EventToUI.texture(
|
||||
int field0,
|
||||
bool field1,
|
||||
) = EventToUI_Texture;
|
||||
}
|
||||
|
||||
@@ -33,15 +35,19 @@ class EventToUI_Event implements EventToUI {
|
||||
}
|
||||
|
||||
class EventToUI_Rgba implements EventToUI {
|
||||
const EventToUI_Rgba(final int field0) : this.field = field0;
|
||||
const EventToUI_Rgba(final int field0) : field = field0;
|
||||
final int field;
|
||||
int get field0 => field;
|
||||
}
|
||||
|
||||
class EventToUI_Texture implements EventToUI {
|
||||
const EventToUI_Texture(final int field0) : this.field = field0;
|
||||
final int field;
|
||||
int get field0 => field;
|
||||
const EventToUI_Texture(final int field0, final bool field1)
|
||||
: f0 = field0,
|
||||
f1 = field1;
|
||||
final int f0;
|
||||
final bool f1;
|
||||
int get field0 => f0;
|
||||
bool get field1 => f1;
|
||||
}
|
||||
|
||||
class RustdeskImpl {
|
||||
@@ -142,7 +148,10 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
@@ -178,7 +187,7 @@ class RustdeskImpl {
|
||||
Future<void> sessionToggleOption(
|
||||
{required UuidValue sessionId, required String value, dynamic hint}) {
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ['toggle_option', value]));
|
||||
() => js.context.callMethod('setByName', ['option:toggle', value]));
|
||||
}
|
||||
|
||||
Future<void> sessionTogglePrivacyMode(
|
||||
@@ -187,8 +196,8 @@ class RustdeskImpl {
|
||||
required bool on,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'toggle_option',
|
||||
jsonEncode({implKey, on})
|
||||
'toggle_privacy_mode',
|
||||
jsonEncode({'impl_key': implKey, 'on': on})
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -226,7 +235,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String getLocalKbLayoutType({dynamic hint}) {
|
||||
throw js.context.callMethod('getByName', ['option:local', 'kb_layout']);
|
||||
return js.context.callMethod('getByName', ['option:local', 'kb_layout']);
|
||||
}
|
||||
|
||||
Future<void> setLocalKbLayoutType(
|
||||
@@ -343,7 +352,7 @@ class RustdeskImpl {
|
||||
|
||||
bool sessionIsKeyboardModeSupported(
|
||||
{required UuidValue sessionId, required String mode, dynamic hint}) {
|
||||
return mode == kKeyLegacyMode;
|
||||
return [kKeyLegacyMode, kKeyMapMode].contains(mode);
|
||||
}
|
||||
|
||||
bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
|
||||
@@ -382,14 +391,32 @@ class RustdeskImpl {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'switch_display',
|
||||
jsonEncode({
|
||||
isDesktop: isDesktop,
|
||||
sessionId: sessionId.toString(),
|
||||
value: value
|
||||
'isDesktop': isDesktop,
|
||||
'sessionId': sessionId.toString(),
|
||||
'value': value
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionHandleFlutterKeyEvent(
|
||||
{required UuidValue sessionId,
|
||||
required String character,
|
||||
required int usbHid,
|
||||
required int lockModes,
|
||||
required bool downOrUp,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'flutter_key_event',
|
||||
jsonEncode({
|
||||
'name': character,
|
||||
'usb_hid': usbHid,
|
||||
'lock_modes': lockModes,
|
||||
if (downOrUp) 'down': 'true',
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionHandleFlutterRawKeyEvent(
|
||||
{required UuidValue sessionId,
|
||||
required String name,
|
||||
required int platformCode,
|
||||
@@ -397,7 +424,6 @@ class RustdeskImpl {
|
||||
required int lockModes,
|
||||
required bool downOrUp,
|
||||
dynamic hint}) {
|
||||
// TODO: map mode
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -630,7 +656,15 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetLoginDeviceInfo({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
String userAgent = html.window.navigator.userAgent;
|
||||
String appName = html.window.navigator.appName;
|
||||
String appVersion = html.window.navigator.appVersion;
|
||||
String? platform = html.window.navigator.platform;
|
||||
return jsonEncode({
|
||||
'os': '$userAgent, $appName $appVersion ($platform)',
|
||||
'type': 'Web client',
|
||||
'name': js.context.callMethod('getByName', ['my_name']),
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> mainChangeId({required String newId, dynamic hint}) {
|
||||
@@ -699,11 +733,11 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetAppName({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future.value(mainGetAppNameSync(hint: hint));
|
||||
}
|
||||
|
||||
String mainGetAppNameSync({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return 'RustDesk';
|
||||
}
|
||||
|
||||
String mainUriPrefixSync({dynamic hint}) {
|
||||
@@ -711,7 +745,8 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetLicense({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
// TODO: implement
|
||||
return Future(() => '');
|
||||
}
|
||||
|
||||
Future<String> mainGetVersion({dynamic hint}) {
|
||||
@@ -755,8 +790,9 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<bool> mainIsUsingPublicServer({dynamic hint}) {
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ["is_using_public_server"]));
|
||||
return Future(() =>
|
||||
js.context.callMethod('getByName', ["is_using_public_server"]) ==
|
||||
'true');
|
||||
}
|
||||
|
||||
Future<void> mainDiscover({dynamic hint}) {
|
||||
@@ -823,11 +859,11 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetMyId({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('getByName', ['my_id']));
|
||||
}
|
||||
|
||||
Future<String> mainGetUuid({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('getByName', ['uuid']));
|
||||
}
|
||||
|
||||
Future<String> mainGetPeerOption(
|
||||
@@ -949,10 +985,11 @@ class RustdeskImpl {
|
||||
|
||||
Future<void> mainSetUserDefaultOption(
|
||||
{required String key, required String value, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', [
|
||||
js.context.callMethod('setByName', [
|
||||
'option:user:default',
|
||||
jsonEncode({'name': key, 'value': value})
|
||||
]);
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
String mainGetUserDefaultOption({required String key, dynamic hint}) {
|
||||
@@ -1026,7 +1063,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetLangs({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('getByName', ['langs']));
|
||||
}
|
||||
|
||||
Future<String> mainGetTemporaryPassword({dynamic hint}) {
|
||||
@@ -1038,7 +1075,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetFingerprint({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future.value('');
|
||||
}
|
||||
|
||||
Future<String> cmGetClientsState({dynamic hint}) {
|
||||
@@ -1080,7 +1117,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainSupportedHwdecodings({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return '{}';
|
||||
}
|
||||
|
||||
Future<bool> mainIsRoot({dynamic hint}) {
|
||||
@@ -1167,8 +1204,10 @@ class RustdeskImpl {
|
||||
required int index,
|
||||
required bool on,
|
||||
dynamic hint}) {
|
||||
// TODO
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'toggle_virtual_display',
|
||||
jsonEncode({'index': index, 'on': on})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> mainSetHomeDir({required String home, dynamic hint}) {
|
||||
@@ -1269,8 +1308,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetBuildDate({dynamic hint}) {
|
||||
// TODO
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('getByName', ['build_date']));
|
||||
}
|
||||
|
||||
String translate(
|
||||
@@ -1412,7 +1450,7 @@ class RustdeskImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mainHideDocker({dynamic hint}) {
|
||||
bool mainHideDock({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -1607,12 +1645,80 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
bool mainIsOptionFixed({required String key, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mainGetUseTextureRender({dynamic hint}) {
|
||||
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();
|
||||
}
|
||||
|
||||
Future<String> getVoiceCallInputDevice({required bool isCm, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> setVoiceCallInputDevice(
|
||||
{required bool isCm, required String device, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool isPresetPasswordMobileOnly({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
String mainGetBuildinOption({required String key, dynamic hint}) {
|
||||
return '';
|
||||
}
|
||||
|
||||
String installInstallOptions({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
int mainMaxEncryptLen({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
sessionRenameFile(
|
||||
{required UuidValue sessionId,
|
||||
required int actId,
|
||||
required String path,
|
||||
required String newName,
|
||||
required bool isRemote,
|
||||
dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:js' as js;
|
||||
import 'dart:html' as html;
|
||||
|
||||
final isAndroid_ = false;
|
||||
final isIOS_ = false;
|
||||
@@ -11,3 +12,9 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
|
||||
final isDesktop_ = false;
|
||||
|
||||
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
|
||||
|
||||
final _userAgent = html.window.navigator.userAgent.toLowerCase();
|
||||
|
||||
final isWebOnWindows_ = _userAgent.contains('win');
|
||||
final isWebOnLinux_ = _userAgent.contains('linux');
|
||||
final isWebOnMacOS_ = _userAgent.contains('mac');
|
||||
|
||||
@@ -58,6 +58,11 @@ class CursorManager {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetSystemCursor() async {
|
||||
latestKey = '';
|
||||
js.context.callMethod('setByName', ['cursor', 'auto']);
|
||||
}
|
||||
}
|
||||
|
||||
class FlutterCustomMemoryImageCursor extends MouseCursor {
|
||||
@@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession {
|
||||
}
|
||||
|
||||
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
|
||||
resetSystemCursor() => CursorManager.instance.resetSystemCursor();
|
||||
|
||||
MouseCursor buildCursorOfCache(
|
||||
model.CursorModel cursor, double scale, model.CursorData? cache) {
|
||||
|
||||
98
flutter/lib/web/settings_page.dart
Normal file
98
flutter/lib/web/settings_page.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/scan_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../models/model.dart';
|
||||
|
||||
class WebSettingsPage extends StatelessWidget {
|
||||
const WebSettingsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isWebDesktop) {
|
||||
return _buildDesktopButton(context);
|
||||
} else {
|
||||
return _buildMobileMenu(context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDesktopButton(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
DesktopSettingPage(initialTabkey: SettingsTabKey.general),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileMenu(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
return PopupMenuButton<String>(
|
||||
tooltip: "",
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
return (isIOS
|
||||
? [
|
||||
const PopupMenuItem(
|
||||
value: "scan",
|
||||
child: Icon(Icons.qr_code_scanner, color: Colors.black),
|
||||
)
|
||||
]
|
||||
: <PopupMenuItem<String>>[]) +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "server",
|
||||
child: Text(translate('ID/Relay Server')),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "login",
|
||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate("Login")
|
||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "about",
|
||||
child: Text(translate('About RustDesk')),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 'server') {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'about') {
|
||||
showAbout(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'login') {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
}
|
||||
if (value == 'scan') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ScanPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class TextureRgbaRenderer {
|
||||
}
|
||||
|
||||
Future<bool> closeTexture(int key) {
|
||||
throw UnimplementedError();
|
||||
return Future(() => true);
|
||||
}
|
||||
|
||||
Future<bool> onRgba(
|
||||
|
||||
@@ -95,17 +95,17 @@ SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
|
||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
|
||||
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||
|
||||
@@ -45,5 +45,7 @@
|
||||
<string>Record the sound from microphone for the purpose of the remote desktop.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>LSUIElement</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</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:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "60773827434eefe6d01eefa814dca9a032b970b3"
|
||||
resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926"
|
||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
@@ -377,9 +377,25 @@ packages:
|
||||
path: "."
|
||||
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
|
||||
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
|
||||
url: "https://github.com/21pages/dynamic_layouts.git"
|
||||
url: "https://github.com/rustdesk-org/dynamic_layouts.git"
|
||||
source: git
|
||||
version: "0.0.1+1"
|
||||
extended_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_text
|
||||
sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
extended_text_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: extended_text_library
|
||||
sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.0"
|
||||
external_path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -509,9 +525,9 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
||||
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
||||
url: "https://github.com/21pages/flutter_gpu_texture_renderer"
|
||||
ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
|
||||
resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
|
||||
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
flutter_improved_scrolling:
|
||||
@@ -1613,5 +1629,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
sdks:
|
||||
dart: ">=3.2.0 <4.0.0"
|
||||
flutter: ">=3.16.0"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# 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
|
||||
version: 1.2.6+44
|
||||
version: 1.3.1+47
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
@@ -79,7 +79,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
||||
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
|
||||
auto_size_text: ^3.0.0
|
||||
bot_toast: ^4.0.3
|
||||
@@ -92,18 +92,19 @@ dependencies:
|
||||
dropdown_button2: ^2.0.0
|
||||
flutter_gpu_texture_renderer:
|
||||
git:
|
||||
url: https://github.com/21pages/flutter_gpu_texture_renderer
|
||||
ref: 38951317afe79d953ab25733667bd96e172a80d3
|
||||
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
||||
ref: 2ded7f146437a761ffe6981e2f742038f85ca68d
|
||||
uuid: ^3.0.7
|
||||
auto_size_text_field: ^2.2.1
|
||||
flex_color_picker: ^3.3.0
|
||||
dynamic_layouts:
|
||||
git:
|
||||
url: https://github.com/21pages/dynamic_layouts.git
|
||||
url: https://github.com/rustdesk-org/dynamic_layouts.git
|
||||
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
|
||||
pull_down_button: ^0.9.3
|
||||
device_info_plus: ^9.1.0
|
||||
qr_flutter: ^4.1.0
|
||||
extended_text: 13.0.0
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
#include <cstdlib> // for getenv and _putenv
|
||||
#include <cstring> // for strcmp
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
@@ -143,6 +146,25 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
|
||||
return OnCreate();
|
||||
}
|
||||
|
||||
static void trySetWindowForeground(HWND window) {
|
||||
char* value = nullptr;
|
||||
size_t size = 0;
|
||||
// Use _dupenv_s to safely get the environment variable
|
||||
_dupenv_s(&value, &size, "SET_FOREGROUND_WINDOW");
|
||||
|
||||
if (value != nullptr) {
|
||||
// Correctly compare the value with "1"
|
||||
if (strcmp(value, "1") == 0) {
|
||||
// Clear the environment variable
|
||||
_putenv("SET_FOREGROUND_WINDOW=");
|
||||
// Set the window to foreground
|
||||
SetForegroundWindow(window);
|
||||
}
|
||||
// Free the duplicated string
|
||||
free(value);
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
@@ -156,6 +178,7 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
trySetWindowForeground(window);
|
||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 460 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 481 KiB |
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
use hbb_common::{allow_err, log};
|
||||
use hbb_common::{allow_err, bail};
|
||||
use hbb_common::{
|
||||
lazy_static,
|
||||
tokio::sync::{
|
||||
@@ -25,6 +25,8 @@ pub use context_send::*;
|
||||
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
|
||||
|
||||
pub(crate) use platform::create_cliprdr_context;
|
||||
|
||||
@@ -130,7 +132,7 @@ impl ClipboardFile {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_stopping_allowed_from_peer(&self) -> bool {
|
||||
pub fn is_beginning_message(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
|
||||
@@ -198,7 +200,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
#[inline]
|
||||
fn send_data(conn_id: i32, data: ClipboardFile) {
|
||||
fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return send_data_to_channel(conn_id, data);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -210,25 +212,28 @@ fn send_data(conn_id: i32, data: ClipboardFile) {
|
||||
}
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
#[inline]
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) {
|
||||
// no need to handle result here
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||
if let Some(msg_channel) = VEC_MSG_CHANNEL
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|x| x.conn_id == conn_id)
|
||||
{
|
||||
allow_err!(msg_channel.sender.send(data));
|
||||
msg_channel.sender.send(data)?;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("conn_id not found");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[inline]
|
||||
fn send_data_to_all(data: ClipboardFile) {
|
||||
// no need to handle result here
|
||||
fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
|
||||
// Need more tests to see if it's necessary to handle the error.
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -795,7 +795,11 @@ impl FuseNode {
|
||||
conn_id: desc.conn_id,
|
||||
stream_id: rand::random(),
|
||||
index: inode as usize - 2,
|
||||
name: desc.name.to_str().unwrap().to_owned(),
|
||||
name: desc
|
||||
.name
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default(),
|
||||
parent: None,
|
||||
attributes: InodeAttributes::from_description(inode, desc),
|
||||
children: Vec::new(),
|
||||
@@ -1140,7 +1144,7 @@ mod fuse_test {
|
||||
}
|
||||
|
||||
fn build_single_file(prefix: &str) {
|
||||
let raw_name = "衬衫的价格为 9 镑 15 便士.txt";
|
||||
let raw_name = "simple_test_file.txt";
|
||||
let f_name = if prefix == "" {
|
||||
raw_name.to_string()
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use crate::{CliprdrError, CliprdrServiceContext};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -52,9 +53,9 @@ pub fn create_cliprdr_context(
|
||||
log::warn!("umount {:?} may fail: {:?}", mnt_path, e);
|
||||
}
|
||||
|
||||
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse().unwrap())?;
|
||||
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?;
|
||||
log::debug!("start cliprdr FUSE");
|
||||
unix_ctx.run().expect("failed to start cliprdr FUSE");
|
||||
unix_ctx.run()?;
|
||||
|
||||
Ok(Box::new(unix_ctx) as Box<_>)
|
||||
}
|
||||
@@ -63,8 +64,10 @@ pub fn create_cliprdr_context(
|
||||
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
struct DummyCliprdrContext {}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl CliprdrServiceContext for DummyCliprdrContext {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
|
||||
Ok(())
|
||||
|
||||
@@ -113,7 +113,7 @@ impl LocalFile {
|
||||
let win32_time = self
|
||||
.last_write_time
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_nanos() as u64
|
||||
/ 100
|
||||
+ LDAP_EPOCH_DELTA;
|
||||
@@ -188,7 +188,7 @@ impl LocalFile {
|
||||
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
|
||||
self.load_handle()?;
|
||||
|
||||
let handle = self.handle.as_mut().unwrap();
|
||||
let handle = self.handle.as_mut()?;
|
||||
|
||||
if offset != self.offset.load(Ordering::Relaxed) {
|
||||
handle
|
||||
@@ -238,9 +238,9 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
||||
})?;
|
||||
|
||||
if mt.is_dir() {
|
||||
let dir = std::fs::read_dir(path).unwrap();
|
||||
let dir = std::fs::read_dir(path)?;
|
||||
for entry in dir {
|
||||
let entry = entry.unwrap();
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
constr_file_lst(&path, file_list, visited)?;
|
||||
}
|
||||
|
||||
@@ -383,13 +383,11 @@ impl ClipboardContext {
|
||||
let file_contents_id = fmt_lst
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)
|
||||
.unwrap();
|
||||
.map(|(id, _)| *id)?;
|
||||
let file_descriptor_id = fmt_lst
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)
|
||||
.unwrap();
|
||||
.map(|(id, _)| *id)?;
|
||||
|
||||
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
|
||||
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::CliprdrError;
|
||||
// url encode and decode is needed
|
||||
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
|
||||
|
||||
pub(super) fn encode_path_to_uri(path: &PathBuf) -> String {
|
||||
let encoded = percent_encoding::percent_encode(path.to_str().unwrap().as_bytes(), &ENCODE_SET)
|
||||
.to_string();
|
||||
pub(super) fn encode_path_to_uri(path: &PathBuf) -> io::Result<String> {
|
||||
let encoded =
|
||||
percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string();
|
||||
format!("file://{}", encoded)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ mod uri_test {
|
||||
#[test]
|
||||
fn test_conversion() {
|
||||
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
|
||||
let uri = super::encode_path_to_uri(&path);
|
||||
let uri = super::encode_path_to_uri(&path).unwrap();
|
||||
assert_eq!(
|
||||
uri,
|
||||
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||
|
||||
@@ -89,7 +89,13 @@ impl SysClipboard for X11Clipboard {
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
*self.former_file_list.lock() = paths.to_vec();
|
||||
|
||||
let uri_list: Vec<String> = paths.iter().map(encode_path_to_uri).collect();
|
||||
let uri_list: Vec<String> = {
|
||||
let mut v = Vec::new();
|
||||
for path in paths {
|
||||
v.push(encode_path_to_uri(path)?);
|
||||
}
|
||||
v
|
||||
};
|
||||
let uri_list = uri_list.join("\n");
|
||||
let text_uri_list_data = uri_list.as_bytes().to_vec();
|
||||
let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat();
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(deref_nullptr)]
|
||||
|
||||
use crate::{
|
||||
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
||||
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
||||
};
|
||||
use hbb_common::log;
|
||||
use std::{
|
||||
boxed::Box,
|
||||
ffi::{CStr, CString},
|
||||
result::Result,
|
||||
};
|
||||
use crate::{
|
||||
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
||||
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
||||
};
|
||||
use hbb_common::log;
|
||||
|
||||
// only used error code will be recorded here
|
||||
/// success
|
||||
@@ -779,7 +779,7 @@ pub fn server_format_list(
|
||||
} else {
|
||||
let n = match CString::new(format.1) {
|
||||
Ok(n) => n,
|
||||
Err(_) => CString::new("").unwrap(),
|
||||
Err(_) => CString::new("").unwrap_or_default(),
|
||||
};
|
||||
CLIPRDR_FORMAT {
|
||||
formatId: format.0 as UINT32,
|
||||
@@ -998,7 +998,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE)
|
||||
}
|
||||
};
|
||||
// no need to handle result here
|
||||
send_data(conn_id as _, data);
|
||||
allow_err!(send_data(conn_id as _, data));
|
||||
|
||||
0
|
||||
}
|
||||
@@ -1045,7 +1045,13 @@ extern "C" fn client_format_list(
|
||||
.iter()
|
||||
.for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone())));
|
||||
} else {
|
||||
send_data(conn_id, data);
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("failed to send format list: {:?}", e);
|
||||
return ERR_CODE_SEND_MSG;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
|
||||
@@ -1067,9 +1073,13 @@ extern "C" fn client_format_list_response(
|
||||
msg_flags
|
||||
);
|
||||
let data = ClipboardFile::FormatListResponse { msg_flags };
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send format list response: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn client_format_data_request(
|
||||
@@ -1090,10 +1100,13 @@ extern "C" fn client_format_data_request(
|
||||
conn_id,
|
||||
requested_format_id
|
||||
);
|
||||
// no need to handle result here
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send format data request: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn client_format_data_response(
|
||||
@@ -1125,9 +1138,13 @@ extern "C" fn client_format_data_response(
|
||||
msg_flags,
|
||||
format_data,
|
||||
};
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send format data response: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn client_file_contents_request(
|
||||
@@ -1175,9 +1192,13 @@ extern "C" fn client_file_contents_request(
|
||||
clip_data_id,
|
||||
};
|
||||
log::debug!("client_file_contents_request called, data: {:?}", &data);
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send file contents request: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn client_file_contents_response(
|
||||
@@ -1213,7 +1234,11 @@ extern "C" fn client_file_contents_response(
|
||||
msg_flags,
|
||||
stream_id
|
||||
);
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send file contents response: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,8 @@ struct wf_clipboard
|
||||
HWND hwnd;
|
||||
HANDLE hmem;
|
||||
HANDLE thread;
|
||||
HANDLE response_data_event;
|
||||
HANDLE formatDataRespEvent;
|
||||
BOOL formatDataRespReceived;
|
||||
|
||||
LPDATAOBJECT data_obj;
|
||||
HANDLE data_obj_mutex;
|
||||
@@ -228,6 +229,7 @@ struct wf_clipboard
|
||||
ULONG req_fsize;
|
||||
char *req_fdata;
|
||||
HANDLE req_fevent;
|
||||
BOOL req_f_received;
|
||||
|
||||
size_t nFiles;
|
||||
size_t file_array_size;
|
||||
@@ -287,6 +289,9 @@ static BOOL try_open_clipboard(HWND hwnd)
|
||||
static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid,
|
||||
void **ppvObject)
|
||||
{
|
||||
if (ppvObject == NULL)
|
||||
return E_INVALIDARG;
|
||||
|
||||
if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown))
|
||||
{
|
||||
IStream_AddRef(This);
|
||||
@@ -362,6 +367,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
|
||||
}
|
||||
|
||||
*pcbRead = clipboard->req_fsize;
|
||||
// Check overflow, can not be a real case
|
||||
if ((instance->m_lOffset.QuadPart + clipboard->req_fsize) < instance->m_lOffset.QuadPart) {
|
||||
// It's better to crash to release the explorer.exe
|
||||
// This is a critical error, because the explorer is waiting for the data
|
||||
// and the m_lOffset is wrong(overflowed)
|
||||
return S_FALSE;
|
||||
}
|
||||
instance->m_lOffset.QuadPart += clipboard->req_fsize;
|
||||
|
||||
if (clipboard->req_fsize < cb)
|
||||
@@ -517,11 +529,17 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **pp
|
||||
|
||||
static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc)
|
||||
{
|
||||
IStream *iStream;
|
||||
IStream *iStream = NULL;
|
||||
BOOL success = FALSE;
|
||||
BOOL isDir = FALSE;
|
||||
CliprdrStream *instance;
|
||||
CliprdrStream *instance = NULL;
|
||||
wfClipboard *clipboard = (wfClipboard *)pData;
|
||||
|
||||
if (!(pData && dsc))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream));
|
||||
|
||||
if (instance)
|
||||
@@ -874,14 +892,18 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This
|
||||
static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count,
|
||||
void *data)
|
||||
{
|
||||
CliprdrDataObject *instance;
|
||||
IDataObject *iDataObject;
|
||||
CliprdrDataObject *instance = NULL;
|
||||
IDataObject *iDataObject = NULL;
|
||||
instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject));
|
||||
|
||||
if (!instance)
|
||||
goto error;
|
||||
|
||||
instance->m_pFormatEtc = NULL;
|
||||
instance->m_pStgMedium = NULL;
|
||||
|
||||
iDataObject = &instance->iDataObject;
|
||||
iDataObject->lpVtbl = NULL;
|
||||
iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl));
|
||||
|
||||
if (!iDataObject->lpVtbl)
|
||||
@@ -929,7 +951,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc
|
||||
|
||||
return instance;
|
||||
error:
|
||||
CliprdrDataObject_Delete(instance);
|
||||
if (iDataObject && iDataObject->lpVtbl)
|
||||
{
|
||||
free(iDataObject->lpVtbl);
|
||||
}
|
||||
if (instance)
|
||||
{
|
||||
if (instance->m_pFormatEtc)
|
||||
{
|
||||
free(instance->m_pFormatEtc);
|
||||
}
|
||||
|
||||
if (instance->m_pStgMedium)
|
||||
{
|
||||
free(instance->m_pStgMedium);
|
||||
}
|
||||
|
||||
CliprdrDataObject_Delete(instance);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1010,6 +1049,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT
|
||||
REFIID riid, void **ppvObject)
|
||||
{
|
||||
(void)This;
|
||||
if (!ppvObject)
|
||||
return E_INVALIDARG;
|
||||
|
||||
if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown))
|
||||
{
|
||||
@@ -1198,6 +1239,7 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
|
||||
WCHAR *unicode_name;
|
||||
#if !defined(UNICODE)
|
||||
size_t size;
|
||||
int towchar_count;
|
||||
#endif
|
||||
|
||||
if (!clipboard || !format_name)
|
||||
@@ -1205,6 +1247,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
|
||||
|
||||
#if defined(UNICODE)
|
||||
unicode_name = _wcsdup(format_name);
|
||||
if (!unicode_name)
|
||||
return 0;
|
||||
#else
|
||||
size = _tcslen(format_name);
|
||||
unicode_name = calloc(size + 1, sizeof(WCHAR));
|
||||
@@ -1212,11 +1256,13 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
|
||||
if (!unicode_name)
|
||||
return 0;
|
||||
|
||||
MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size);
|
||||
#endif
|
||||
|
||||
if (!unicode_name)
|
||||
towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0);
|
||||
if (towchar_count <= 0 || towchar_count > size)
|
||||
return 0;
|
||||
towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size);
|
||||
if (towchar_count <= 0)
|
||||
return 0;
|
||||
#endif
|
||||
|
||||
for (i = 0; i < clipboard->map_size; i++)
|
||||
{
|
||||
@@ -1312,6 +1358,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
||||
if (!clipboard)
|
||||
return -1;
|
||||
|
||||
// to-do:
|
||||
// Directly use the environment variable `TEMP` is not safe.
|
||||
// But this function is not used for now.
|
||||
if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) ==
|
||||
0)
|
||||
return -1;
|
||||
@@ -1444,7 +1493,37 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID)
|
||||
return rc;
|
||||
}
|
||||
|
||||
UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, void **data)
|
||||
// Ensure the event is not signaled, and reset it if it is.
|
||||
UINT try_reset_event(HANDLE event)
|
||||
{
|
||||
if (!event)
|
||||
{
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
DWORD result = WaitForSingleObject(event, 0);
|
||||
if (result == WAIT_OBJECT_0)
|
||||
{
|
||||
if (!ResetEvent(event))
|
||||
{
|
||||
return GetLastError();
|
||||
}
|
||||
else
|
||||
{
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
else if (result == WAIT_TIMEOUT)
|
||||
{
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data)
|
||||
{
|
||||
UINT rc = ERROR_SUCCESS;
|
||||
clipboard->context->IsStopped = FALSE;
|
||||
@@ -1456,7 +1535,21 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
|
||||
DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis);
|
||||
if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE)
|
||||
{
|
||||
continue;
|
||||
if ((*recvedFlag) == TRUE) {
|
||||
// The data has been received, but the event is still not signaled.
|
||||
// We just skip the rest of the waiting and reset the flag.
|
||||
*recvedFlag = FALSE;
|
||||
// Explicitly set the waitRes to WAIT_OBJECT_0, because we have received the data.
|
||||
waitRes = WAIT_OBJECT_0;
|
||||
} else {
|
||||
// The data has not been received yet, we should continue to wait.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ResetEvent(event))
|
||||
{
|
||||
// NOTE: critical error here, crash may be better
|
||||
}
|
||||
|
||||
if (clipboard->context->IsStopped == TRUE)
|
||||
@@ -1470,12 +1563,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
if (!ResetEvent(event))
|
||||
{
|
||||
// NOTE: critical error here, crash may be better
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
if ((*data) == NULL)
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
@@ -1519,6 +1606,13 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
rc = try_reset_event(clipboard->formatDataRespEvent);
|
||||
if (rc != ERROR_SUCCESS)
|
||||
{
|
||||
return rc;
|
||||
}
|
||||
clipboard->formatDataRespReceived = FALSE;
|
||||
|
||||
remoteFormatId = get_remote_format_id(clipboard, formatId);
|
||||
|
||||
formatDataRequest.connID = connID;
|
||||
@@ -1530,7 +1624,7 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
||||
return rc;
|
||||
}
|
||||
|
||||
wait_response_event(connID, clipboard, clipboard->response_data_event, &clipboard->hmem);
|
||||
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
||||
}
|
||||
|
||||
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
|
||||
@@ -1543,7 +1637,17 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
rc = try_reset_event(clipboard->req_fevent);
|
||||
if (rc != ERROR_SUCCESS)
|
||||
{
|
||||
return rc;
|
||||
}
|
||||
clipboard->req_f_received = FALSE;
|
||||
|
||||
fileContentsRequest.connID = connID;
|
||||
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
|
||||
// But it is OK, because it is only used to check if the stream is the same in
|
||||
// `wf_cliprdr_server_file_contents_request()` function.
|
||||
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
|
||||
fileContentsRequest.listIndex = index;
|
||||
fileContentsRequest.dwFlags = flag;
|
||||
@@ -1558,7 +1662,7 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
||||
return rc;
|
||||
}
|
||||
|
||||
return wait_response_event(connID, clipboard, clipboard->req_fevent, (void **)&clipboard->req_fdata);
|
||||
return wait_response_event(connID, clipboard, clipboard->req_fevent, &clipboard->req_f_received, (void **)&clipboard->req_fdata);
|
||||
}
|
||||
|
||||
static UINT cliprdr_send_response_filecontents(
|
||||
@@ -1788,6 +1892,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM
|
||||
break;
|
||||
|
||||
case WM_DESTROYCLIPBOARD:
|
||||
// to-do: clear clipboard data?
|
||||
case WM_ASKCBFORMATNAME:
|
||||
case WM_HSCROLLCLIPBOARD:
|
||||
case WM_PAINTCLIPBOARD:
|
||||
@@ -1904,7 +2009,7 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po
|
||||
LONG positionHigh, DWORD nRequested, DWORD *puSize)
|
||||
{
|
||||
BOOL res = FALSE;
|
||||
HANDLE hFile;
|
||||
HANDLE hFile = NULL;
|
||||
DWORD nGet, rc;
|
||||
|
||||
if (!file_name || !buffer || !puSize)
|
||||
@@ -1932,9 +2037,11 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po
|
||||
|
||||
res = TRUE;
|
||||
error:
|
||||
|
||||
if (!CloseHandle(hFile))
|
||||
res = FALSE;
|
||||
if (hFile)
|
||||
{
|
||||
if (!CloseHandle(hFile))
|
||||
res = FALSE;
|
||||
}
|
||||
|
||||
if (res)
|
||||
*puSize = nGet;
|
||||
@@ -1945,8 +2052,8 @@ error:
|
||||
/* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */
|
||||
static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen)
|
||||
{
|
||||
HANDLE hFile;
|
||||
FILEDESCRIPTORW *fd;
|
||||
HANDLE hFile = NULL;
|
||||
FILEDESCRIPTORW *fd = NULL;
|
||||
fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW));
|
||||
|
||||
if (!fd)
|
||||
@@ -1975,7 +2082,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
|
||||
}
|
||||
|
||||
fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh);
|
||||
wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen);
|
||||
if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0]))
|
||||
{
|
||||
// The file name is too long, which is not a normal case.
|
||||
// So we just return NULL.
|
||||
CloseHandle(hFile);
|
||||
free(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
wcsncpy_s(fd->cFileName, sizeof(fd->cFileName) / sizeof(fd->cFileName[0]), file_name + pathLen, wcslen(file_name + pathLen) + 1);
|
||||
CloseHandle(hFile);
|
||||
|
||||
return fd;
|
||||
@@ -2024,7 +2140,12 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
|
||||
if (!clipboard->file_names[clipboard->nFiles])
|
||||
return FALSE;
|
||||
|
||||
wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name);
|
||||
// `MAX_PATH` is long enough for the file name.
|
||||
// So we just return FALSE if the file name is too long, which is not a normal case.
|
||||
if ((wcslen(full_file_name) + 1) > MAX_PATH)
|
||||
return FALSE;
|
||||
|
||||
wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1);
|
||||
/* add to descriptor array */
|
||||
clipboard->fileDescriptor[clipboard->nFiles] =
|
||||
wf_cliprdr_get_file_descriptor(full_file_name, pathLen);
|
||||
@@ -2048,8 +2169,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
|
||||
if (!clipboard || !Dir)
|
||||
return FALSE;
|
||||
|
||||
// StringCchCopy(DirSpec, MAX_PATH, Dir);
|
||||
// StringCchCat(DirSpec, MAX_PATH, TEXT("\\*"));
|
||||
if (wcslen(Dir) + 3 > MAX_PATH)
|
||||
return FALSE;
|
||||
StringCchCopyW(DirSpec, MAX_PATH, Dir);
|
||||
StringCchCatW(DirSpec, MAX_PATH, L"\\*");
|
||||
|
||||
@@ -2078,9 +2199,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
|
||||
if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
||||
{
|
||||
WCHAR DirAdd[MAX_PATH];
|
||||
// StringCchCopy(DirAdd, MAX_PATH, Dir);
|
||||
// StringCchCat(DirAdd, MAX_PATH, _T("\\"));
|
||||
// StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName);
|
||||
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
|
||||
return FALSE;
|
||||
StringCchCopyW(DirAdd, MAX_PATH, Dir);
|
||||
StringCchCatW(DirAdd, MAX_PATH, L"\\");
|
||||
StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName);
|
||||
@@ -2094,10 +2214,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
|
||||
else
|
||||
{
|
||||
WCHAR fileName[MAX_PATH];
|
||||
// StringCchCopy(fileName, MAX_PATH, Dir);
|
||||
// StringCchCat(fileName, MAX_PATH, _T("\\"));
|
||||
// StringCchCat(fileName, MAX_PATH, FindFileData.cFileName);
|
||||
|
||||
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
|
||||
return FALSE;
|
||||
StringCchCopyW(fileName, MAX_PATH, Dir);
|
||||
StringCchCatW(fileName, MAX_PATH, L"\\");
|
||||
StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName);
|
||||
@@ -2242,9 +2360,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
if (context->EnableFiles)
|
||||
{
|
||||
UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32));
|
||||
*p_conn_id = formatList->connID;
|
||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
|
||||
rc = CHANNEL_RC_OK;
|
||||
if (p_conn_id) {
|
||||
*p_conn_id = formatList->connID;
|
||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
|
||||
rc = CHANNEL_RC_OK;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2265,16 +2385,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
// SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL);
|
||||
|
||||
FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS));
|
||||
format_ids->connID = formatList->connID;
|
||||
format_ids->size = (UINT32)clipboard->map_size;
|
||||
format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
|
||||
for (i = 0; i < format_ids->size; ++i)
|
||||
if (format_ids)
|
||||
{
|
||||
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id;
|
||||
}
|
||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids))
|
||||
{
|
||||
rc = CHANNEL_RC_OK;
|
||||
format_ids->connID = formatList->connID;
|
||||
format_ids->size = (UINT32)clipboard->map_size;
|
||||
format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
|
||||
if (format_ids->formats)
|
||||
{
|
||||
for (i = 0; i < format_ids->size; ++i)
|
||||
{
|
||||
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id;
|
||||
}
|
||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids))
|
||||
{
|
||||
rc = CHANNEL_RC_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2469,17 +2603,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
|
||||
p += len + 1, clipboard->nFiles++)
|
||||
{
|
||||
int cchWideChar;
|
||||
WCHAR *wFileName;
|
||||
cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0);
|
||||
wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR));
|
||||
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar);
|
||||
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar);
|
||||
if (wFileName)
|
||||
{
|
||||
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar);
|
||||
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar);
|
||||
free(wFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
GlobalUnlock(stg_medium.hGlobal);
|
||||
ReleaseStgMedium(&stg_medium);
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalUnlock(stg_medium.hGlobal);
|
||||
ReleaseStgMedium(&stg_medium);
|
||||
resp:
|
||||
// size will not overflow, because size type is size_t (unsigned __int64)
|
||||
size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW);
|
||||
groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size);
|
||||
|
||||
@@ -2519,10 +2664,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
|
||||
globlemem = (char *)GlobalLock(hClipdata);
|
||||
size = (int)GlobalSize(hClipdata);
|
||||
buff = malloc(size);
|
||||
CopyMemory(buff, globlemem, size);
|
||||
if (buff)
|
||||
{
|
||||
CopyMemory(buff, globlemem, size);
|
||||
rc = ERROR_SUCCESS;
|
||||
}
|
||||
else
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
GlobalUnlock(hClipdata);
|
||||
CloseClipboard();
|
||||
rc = ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2545,7 +2697,7 @@ exit:
|
||||
response.requestedFormatData = (BYTE *)buff;
|
||||
if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response))
|
||||
{
|
||||
// CAUTION: if failed to send, server will wait a long time
|
||||
// CAUTION: if failed to send, server will wait a long time, default 30 seconds.
|
||||
}
|
||||
|
||||
if (buff)
|
||||
@@ -2621,9 +2773,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
|
||||
rc = CHANNEL_RC_OK;
|
||||
} while (0);
|
||||
|
||||
if (!SetEvent(clipboard->response_data_event))
|
||||
if (!SetEvent(clipboard->formatDataRespEvent))
|
||||
{
|
||||
// CAUTION: critical error here, process will hang up until wait timeout default 3min.
|
||||
// If failed to set event, set flag to indicate the event is received.
|
||||
DEBUG_CLIPRDR("wf_cliprdr_server_format_data_response(), SetEvent failed with 0x%x", GetLastError());
|
||||
clipboard->formatDataRespReceived = TRUE;
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
return rc;
|
||||
@@ -2899,7 +3053,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
|
||||
if (!SetEvent(clipboard->req_fevent))
|
||||
{
|
||||
// CAUTION: critical error here, process will hang up until wait timeout default 3min.
|
||||
// If failed to set event, set flag to indicate the event is received.
|
||||
DEBUG_CLIPRDR("wf_cliprdr_server_file_contents_response(), SetEvent failed with 0x%x", GetLastError());
|
||||
clipboard->req_f_received = TRUE;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
@@ -2934,14 +3090,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
|
||||
(formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping))))
|
||||
goto error;
|
||||
|
||||
if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
||||
if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
||||
goto error;
|
||||
clipboard->formatDataRespReceived = FALSE;
|
||||
|
||||
if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex")))
|
||||
goto error;
|
||||
|
||||
if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
||||
goto error;
|
||||
clipboard->req_f_received = FALSE;
|
||||
|
||||
if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL)))
|
||||
goto error;
|
||||
@@ -3002,8 +3160,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
|
||||
clipboard->data_obj = NULL;
|
||||
}
|
||||
|
||||
if (clipboard->response_data_event)
|
||||
CloseHandle(clipboard->response_data_event);
|
||||
if (clipboard->formatDataRespEvent)
|
||||
CloseHandle(clipboard->formatDataRespEvent);
|
||||
|
||||
if (clipboard->data_obj_mutex)
|
||||
CloseHandle(clipboard->data_obj_mutex);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user