mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-16 01:31:01 +03:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0450db203 | ||
|
|
3a75947553 | ||
|
|
c565849062 | ||
|
|
40e8f0d307 | ||
|
|
129f6c869b | ||
|
|
924aa515c6 | ||
|
|
c51771c854 | ||
|
|
c8b9031996 | ||
|
|
4da584055d | ||
|
|
bd22b01370 | ||
|
|
b35b48086a | ||
|
|
445e9ac285 | ||
|
|
7a3e1fe648 | ||
|
|
dfa9519d58 | ||
|
|
cc6f919080 | ||
|
|
2cdaca0fa3 | ||
|
|
6159449eba | ||
|
|
6088920f8d | ||
|
|
e8187588c1 | ||
|
|
289076aa70 | ||
|
|
547da31095 | ||
|
|
1bf4ef1f46 | ||
|
|
1212d9fa2d | ||
|
|
8c8a643cce | ||
|
|
675ffe0381 | ||
|
|
844caf8c15 | ||
|
|
0f6d28def7 | ||
|
|
0d3243e6dd | ||
|
|
53d11e99d7 | ||
|
|
defb3e6c73 | ||
|
|
ae8dfe84a0 | ||
|
|
5e920f0fd0 | ||
|
|
1a0814b201 | ||
|
|
ace98d98ad | ||
|
|
09083b3afa | ||
|
|
36e11c61a9 | ||
|
|
55187e9243 | ||
|
|
ae1c1a56e6 | ||
|
|
cdd58e77eb | ||
|
|
ce924cc0d3 | ||
|
|
498b8ba3d6 | ||
|
|
af610b2408 | ||
|
|
6cdbcfc082 | ||
|
|
9c7f51bc76 | ||
|
|
65683cc3e6 | ||
|
|
eb1ef0969c | ||
|
|
29b01e9cef | ||
|
|
cde7620eda | ||
|
|
844b853074 | ||
|
|
97f02ed25e | ||
|
|
22c84bbbd1 | ||
|
|
227f154ee7 | ||
|
|
59d7bf1e86 | ||
|
|
38fcf4e039 | ||
|
|
4b3b31147e | ||
|
|
e6d4067f48 | ||
|
|
507de628c9 | ||
|
|
2591d4f044 | ||
|
|
9bcd0d1b03 | ||
|
|
5555ba6b2f | ||
|
|
28b6bc186f | ||
|
|
00d38260e1 | ||
|
|
e06f456bbd | ||
|
|
cc860b2906 | ||
|
|
839e8180e0 | ||
|
|
83aba804d0 | ||
|
|
560c1effe8 | ||
|
|
e7353be0cd | ||
|
|
ba832362a7 | ||
|
|
9ea09c1515 | ||
|
|
3a97b63e95 | ||
|
|
dec3cde9b3 | ||
|
|
c4d0b02478 | ||
|
|
306dd77b81 | ||
|
|
fd62751cb8 | ||
|
|
b0edfb8f70 | ||
|
|
334526026c | ||
|
|
b5414ec002 | ||
|
|
4eca8b9447 | ||
|
|
1ebc726acd | ||
|
|
d563372a91 | ||
|
|
4a745d82f6 | ||
|
|
2f5f701dc7 | ||
|
|
60a0099ba0 | ||
|
|
30a7847100 | ||
|
|
1e822fa135 | ||
|
|
f6261883e8 | ||
|
|
3365844def | ||
|
|
769bbf1e1c | ||
|
|
81b999cfbe | ||
|
|
9959217cc3 | ||
|
|
3e6938bec6 | ||
|
|
4459406578 | ||
|
|
beb1084e87 | ||
|
|
d4184fd865 | ||
|
|
ffc73f86a0 | ||
|
|
c74bdcdfdb | ||
|
|
6d8b5b289f | ||
|
|
1d6873f622 | ||
|
|
7c55e3266b | ||
|
|
ce5151032e | ||
|
|
ba88bc9e8b | ||
|
|
e0095aebda | ||
|
|
664a3e186e | ||
|
|
e4f7e126e5 | ||
|
|
49989e34e4 | ||
|
|
75a14fea23 | ||
|
|
f535406962 | ||
|
|
f3f3bb538f | ||
|
|
8fefd34c15 | ||
|
|
d98f947824 | ||
|
|
5f52ce2c1b | ||
|
|
1d799483d7 | ||
|
|
3db55a718c | ||
|
|
a516f01feb | ||
|
|
2e314bf032 | ||
|
|
b93d4ce3fc | ||
|
|
21bcfd173d | ||
|
|
3d5262c36f | ||
|
|
cfd801c5d6 | ||
|
|
216a72592d | ||
|
|
ddd3401bd7 | ||
|
|
47139edd81 | ||
|
|
c6e3f60a6b | ||
|
|
88a99211f3 | ||
|
|
d08c335fdf | ||
|
|
e5ec6957fe | ||
|
|
e20f5dd001 | ||
|
|
e1a6ccc100 | ||
|
|
cc288272d3 | ||
|
|
49ce4edb8a | ||
|
|
29c3b29bda | ||
|
|
8a8f708c3e | ||
|
|
c5038b1a78 | ||
|
|
f4c038ea93 | ||
|
|
d9ea717056 | ||
|
|
40af9dc78b | ||
|
|
81fc22a156 | ||
|
|
2e7bd26e4c | ||
|
|
179b562472 | ||
|
|
ab246fdcbf | ||
|
|
d65d3b7326 | ||
|
|
9f9a22ec63 | ||
|
|
a8f1a66043 | ||
|
|
0b3e7bf33e | ||
|
|
c358399eca | ||
|
|
cacca7295c | ||
|
|
d2e98cc620 | ||
|
|
2e81bcb447 | ||
|
|
cbca0eb340 | ||
|
|
9380f33d7c | ||
|
|
519539ed0a | ||
|
|
1f2a75fbd8 | ||
|
|
51055a7e5b | ||
|
|
13effe7f14 | ||
|
|
943f96ef8c | ||
|
|
260a82ee5c | ||
|
|
a2792d1527 | ||
|
|
2922ebe22a | ||
|
|
1e6944b380 | ||
|
|
993862c103 | ||
|
|
c8cd564e69 | ||
|
|
a4cd64f0d5 | ||
|
|
f0ca4b9fee | ||
|
|
aa3402b44a | ||
|
|
26ebd0deb9 | ||
|
|
4150036589 | ||
|
|
7a1157f1b0 | ||
|
|
3bd34bf0b9 | ||
|
|
5f29016861 | ||
|
|
e40243b55d | ||
|
|
dbbbd08934 | ||
|
|
29e12b84a9 | ||
|
|
04c0f66ca9 | ||
|
|
ec28567362 | ||
|
|
d4377a13c5 | ||
|
|
39e713838f | ||
|
|
75a4671bda | ||
|
|
827efabbc0 | ||
|
|
532fe6aefb | ||
|
|
ae339f039d | ||
|
|
bf390611ab | ||
|
|
e3f6829d02 | ||
|
|
832002a10f | ||
|
|
d335cdbb0c | ||
|
|
6a5d5875c8 | ||
|
|
cf06d1028f | ||
|
|
fd178a7b6c | ||
|
|
f3a2733d75 | ||
|
|
55de573a01 | ||
|
|
40239a1c41 | ||
|
|
c68ce7dd84 | ||
|
|
690a2c8399 | ||
|
|
4b4fd94f3e | ||
|
|
5abe42f66c | ||
|
|
48aec6484c | ||
|
|
a946d4d0c9 | ||
|
|
24f4b94082 | ||
|
|
aa1e122532 | ||
|
|
d400999b9c | ||
|
|
1d416f6626 | ||
|
|
9d9741f18e | ||
|
|
50aa8e12ad | ||
|
|
5931af460e | ||
|
|
fc607d6789 | ||
|
|
529e70910d | ||
|
|
f300d797e2 | ||
|
|
e3cce2824d | ||
|
|
f34b8411a7 | ||
|
|
8745fcbb6a |
2
.github/workflows/bridge.yml
vendored
2
.github/workflows/bridge.yml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: "3.16.9"
|
FLUTTER_VERSION: "3.19.6"
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/flutter-build.yml
vendored
26
.github/workflows/flutter-build.yml
vendored
@@ -33,8 +33,8 @@ env:
|
|||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.07.12
|
# vcpkg version: 2024.07.12
|
||||||
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
|
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
|
||||||
VERSION: "1.3.0"
|
VERSION: "1.3.2"
|
||||||
NDK_VERSION: "r27"
|
NDK_VERSION: "r27b"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||||
@@ -884,7 +884,7 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
g++-multilib \
|
g++-multilib \
|
||||||
libappindicator3-dev \
|
libayatana-appindicator3-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libc6-dev \
|
libc6-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
@@ -976,8 +976,11 @@ jobs:
|
|||||||
- name: fix android for flutter 3.13
|
- name: fix android for flutter 3.13
|
||||||
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||||
run: |
|
run: |
|
||||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
cd flutter
|
||||||
cd flutter/lib
|
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
|
||||||
|
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
|
||||||
|
flutter pub get
|
||||||
|
cd lib
|
||||||
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
|
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
|
||||||
|
|
||||||
- name: Build rustdesk lib
|
- name: Build rustdesk lib
|
||||||
@@ -1144,7 +1147,7 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
g++-multilib \
|
g++-multilib \
|
||||||
libappindicator3-dev \
|
libayatana-appindicator3-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libc6-dev \
|
libc6-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
@@ -1210,8 +1213,11 @@ jobs:
|
|||||||
- name: fix android for flutter 3.13
|
- name: fix android for flutter 3.13
|
||||||
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||||
run: |
|
run: |
|
||||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
cd flutter
|
||||||
cd flutter/lib
|
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
|
||||||
|
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
|
||||||
|
flutter pub get
|
||||||
|
cd lib
|
||||||
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
|
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
|
||||||
|
|
||||||
- name: Build rustdesk
|
- name: Build rustdesk
|
||||||
@@ -1418,7 +1424,7 @@ jobs:
|
|||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
libappindicator3-dev \
|
libayatana-appindicator3-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
@@ -1675,7 +1681,7 @@ jobs:
|
|||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
libappindicator3-dev \
|
libayatana-appindicator3-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libclang-dev \
|
libclang-dev \
|
||||||
libdbus-1-dev \
|
libdbus-1-dev \
|
||||||
|
|||||||
4
.github/workflows/playground.yml
vendored
4
.github/workflows/playground.yml
vendored
@@ -18,7 +18,7 @@ env:
|
|||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.06.15
|
# vcpkg version: 2024.06.15
|
||||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||||
VERSION: "1.3.0"
|
VERSION: "1.3.2"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -262,7 +262,7 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
g++-multilib \
|
g++-multilib \
|
||||||
libappindicator3-dev \
|
libayatana-appindicator3-dev\
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libc6-dev \
|
libc6-dev \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -54,3 +54,4 @@ examples/**/target/
|
|||||||
vcpkg_installed
|
vcpkg_installed
|
||||||
flutter/lib/generated_plugin_registrant.dart
|
flutter/lib/generated_plugin_registrant.dart
|
||||||
libsciter.dylib
|
libsciter.dylib
|
||||||
|
flutter/web/
|
||||||
33
Cargo.lock
generated
33
Cargo.lock
generated
@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "arboard"
|
name = "arboard"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
source = "git+https://github.com/rustdesk-org/arboard#a04bdb1b368a99691822c33bf0f7ed497d6a7a35"
|
source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clipboard-win",
|
"clipboard-win",
|
||||||
"core-graphics 0.23.2",
|
"core-graphics 0.23.2",
|
||||||
@@ -860,6 +860,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.38"
|
version = "0.4.38"
|
||||||
@@ -3045,7 +3051,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "hwcodec"
|
name = "hwcodec"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "git+https://github.com/rustdesk-org/hwcodec#6abd1898f3a03481ed0c038507b5218d6ea94267"
|
source = "git+https://github.com/rustdesk-org/hwcodec#8bbd05bb300ad07cc345356ad85570f9ea99fbfa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -3967,11 +3973,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.1.1",
|
||||||
"libc",
|
"libc",
|
||||||
"memoffset 0.9.1",
|
"memoffset 0.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"cfg_aliases 0.2.1",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@@ -5187,7 +5205,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rdev"
|
name = "rdev"
|
||||||
version = "0.5.0-2"
|
version = "0.5.0-2"
|
||||||
source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488"
|
source = "git+https://github.com/rustdesk-org/rdev#961d25cc00c6b3ef80f444e6a7bed9872e2c35ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa 0.24.1",
|
"cocoa 0.24.1",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
@@ -5462,7 +5480,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.3.0"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -5494,6 +5512,7 @@ dependencies = [
|
|||||||
"flutter_rust_bridge",
|
"flutter_rust_bridge",
|
||||||
"fon",
|
"fon",
|
||||||
"fruitbasket",
|
"fruitbasket",
|
||||||
|
"gtk",
|
||||||
"hbb_common",
|
"hbb_common",
|
||||||
"hex",
|
"hex",
|
||||||
"hound",
|
"hound",
|
||||||
@@ -5508,6 +5527,7 @@ dependencies = [
|
|||||||
"libpulse-simple-binding",
|
"libpulse-simple-binding",
|
||||||
"mac_address",
|
"mac_address",
|
||||||
"magnum-opus",
|
"magnum-opus",
|
||||||
|
"nix 0.29.0",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"objc",
|
"objc",
|
||||||
"objc_id",
|
"objc_id",
|
||||||
@@ -5539,6 +5559,7 @@ dependencies = [
|
|||||||
"system_shutdown",
|
"system_shutdown",
|
||||||
"tao",
|
"tao",
|
||||||
"tauri-winrt-notification",
|
"tauri-winrt-notification",
|
||||||
|
"termios",
|
||||||
"totp-rs",
|
"totp-rs",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"url",
|
"url",
|
||||||
@@ -5559,7 +5580,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.3.0"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.3.0"
|
version = "1.3.2"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -161,6 +161,9 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/
|
|||||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||||
percent-encoding = {version = "2.3", optional = true}
|
percent-encoding = {version = "2.3", optional = true}
|
||||||
once_cell = {version = "1.18", optional = true}
|
once_cell = {version = "1.18", optional = true}
|
||||||
|
nix = { version = "0.29", features = ["term", "process"]}
|
||||||
|
gtk = "0.18"
|
||||||
|
termios = "0.3"
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.13"
|
android_logger = "0.13"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||||
<a href="#free-public-servers">Servers</a> •
|
<a href="#public-servers">Servers</a> •
|
||||||
<a href="#raw-steps-to-build">Build</a> •
|
<a href="#raw-steps-to-build">Build</a> •
|
||||||
<a href="#how-to-build-with-docker">Docker</a> •
|
<a href="#how-to-build-with-docker">Docker</a> •
|
||||||
<a href="#file-structure">Structure</a> •
|
<a href="#file-structure">Structure</a> •
|
||||||
@@ -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
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.3.0
|
version: 1.3.2
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.3.0
|
version: 1.3.2
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
9
build.py
9
build.py
@@ -283,11 +283,14 @@ def generate_control_file(version):
|
|||||||
system2('/bin/rm -rf %s' % control_file_path)
|
system2('/bin/rm -rf %s' % control_file_path)
|
||||||
|
|
||||||
content = """Package: rustdesk
|
content = """Package: rustdesk
|
||||||
|
Section: net
|
||||||
|
Priority: optional
|
||||||
Version: %s
|
Version: %s
|
||||||
Architecture: %s
|
Architecture: %s
|
||||||
Maintainer: rustdesk <info@rustdesk.com>
|
Maintainer: rustdesk <info@rustdesk.com>
|
||||||
Homepage: https://rustdesk.com
|
Homepage: https://rustdesk.com
|
||||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire%s
|
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.
|
Description: A remote control software.
|
||||||
|
|
||||||
""" % (version, get_deb_arch(), get_deb_extra_depends())
|
""" % (version, get_deb_arch(), get_deb_extra_depends())
|
||||||
@@ -330,8 +333,6 @@ def build_flutter_deb(version, features):
|
|||||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||||
system2(
|
|
||||||
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
|
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||||
system2(
|
system2(
|
||||||
@@ -375,8 +376,6 @@ def build_deb_from_folder(version, binary_folder):
|
|||||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||||
system2(
|
system2(
|
||||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||||
system2(
|
|
||||||
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
|
|
||||||
system2(
|
system2(
|
||||||
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
||||||
|
|
||||||
|
|||||||
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,20 +1,18 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
|
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
|
||||||
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> •
|
<a href="#публічні-сервери">Сервери</a> •
|
||||||
<a href="#кроки-для-збірки">Збирання</a> •
|
<a href="#кроки-для-збірки">Збирання</a> •
|
||||||
<a href="#як-зібрати-за-допомогою-docker">Docker</a> •
|
<a href="#як-зібрати-за-допомогою-docker">Docker</a> •
|
||||||
<a href="#структура-файлів">Структура</a> •
|
<a href="#структура-файлів">Структура</a> •
|
||||||
<a href="#знімки">Знімки</a><br>
|
<a href="#знімки-екрана">Знімки екрана</a><br>
|
||||||
[<a href="../README.md">English</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
|
[<a href="../README.md">English</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
|
||||||
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk на вашу рідну мову</B>
|
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||||
|
|
||||||
[](https://ko-fi.com/I2I04VU09)
|
[](https://ko-fi.com/I2I04VU09)
|
||||||
|
|
||||||
[](https://console.algora.io/org/rustdesk/bounties?status=open)
|
|
||||||
|
|
||||||
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
|
||||||
|
|
||||||

|

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

|

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

|

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

|

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

|

|
||||||
|
|
||||||
|
## [Публічні сервери](#публічні-сервери)
|
||||||
|
|
||||||
|
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||||
|
|
||||||
[](https://ko-fi.com/I2I04VU09)
|
[](https://ko-fi.com/I2I04VU09)
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md).
|
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md).
|
||||||
|
|
||||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||||
|
|
||||||
@@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m
|
|||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。
|
桌面版本使用 Flutter 或 Sciter(已弃用)作为 GUI,本教程仅适用于 Sciter,因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本。
|
||||||
|
|
||||||
|
请自行下载Sciter动态库。
|
||||||
|
|
||||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||||
@@ -207,12 +209,13 @@ target/release/rustdesk
|
|||||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
|
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
|
||||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
|
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
|
||||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
|
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
|
||||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现
|
||||||
|
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI(已弃用)
|
||||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
|
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
|
||||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
|
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
|
||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
|
||||||
|
|
||||||
## 截图
|
## 截图
|
||||||
|
|||||||
1
flutter/assets/message_24dp_5F6368.svg
Normal file
1
flutter/assets/message_24dp_5F6368.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="-4 -4 32 32" width="24px" fill="#5f6368"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
@@ -302,6 +302,7 @@ prebuild)
|
|||||||
|
|
||||||
sed \
|
sed \
|
||||||
-i \
|
-i \
|
||||||
|
-e 's/extended_text: .*/extended_text: 11.1.0/' \
|
||||||
-e 's/uni_links_desktop/#uni_links_desktop/g' \
|
-e 's/uni_links_desktop/#uni_links_desktop/g' \
|
||||||
flutter/pubspec.yaml
|
flutter/pubspec.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart';
|
|||||||
import 'mobile/pages/file_manager_page.dart';
|
import 'mobile/pages/file_manager_page.dart';
|
||||||
import 'mobile/pages/remote_page.dart';
|
import 'mobile/pages/remote_page.dart';
|
||||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||||
|
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
|
||||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||||
import 'models/model.dart';
|
import 'models/model.dart';
|
||||||
import 'models/platform_model.dart';
|
import 'models/platform_model.dart';
|
||||||
@@ -50,6 +51,9 @@ final isLinux = isLinux_;
|
|||||||
final isDesktop = isDesktop_;
|
final isDesktop = isDesktop_;
|
||||||
final isWeb = isWeb_;
|
final isWeb = isWeb_;
|
||||||
final isWebDesktop = isWebDesktop_;
|
final isWebDesktop = isWebDesktop_;
|
||||||
|
final isWebOnWindows = isWebOnWindows_;
|
||||||
|
final isWebOnLinux = isWebOnLinux_;
|
||||||
|
final isWebOnMacOs = isWebOnMacOS_;
|
||||||
var isMobile = isAndroid || isIOS;
|
var isMobile = isAndroid || isIOS;
|
||||||
var version = '';
|
var version = '';
|
||||||
int androidVersion = 0;
|
int androidVersion = 0;
|
||||||
@@ -347,6 +351,9 @@ class MyTheme {
|
|||||||
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
||||||
scaffoldBackgroundColor: Colors.white,
|
scaffoldBackgroundColor: Colors.white,
|
||||||
dialogBackgroundColor: Colors.white,
|
dialogBackgroundColor: Colors.white,
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
),
|
||||||
dialogTheme: DialogTheme(
|
dialogTheme: DialogTheme(
|
||||||
elevation: 15,
|
elevation: 15,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -442,6 +449,9 @@ class MyTheme {
|
|||||||
hoverColor: Color.fromARGB(255, 45, 46, 53),
|
hoverColor: Color.fromARGB(255, 45, 46, 53),
|
||||||
scaffoldBackgroundColor: Color(0xFF18191E),
|
scaffoldBackgroundColor: Color(0xFF18191E),
|
||||||
dialogBackgroundColor: Color(0xFF18191E),
|
dialogBackgroundColor: Color(0xFF18191E),
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
),
|
||||||
dialogTheme: DialogTheme(
|
dialogTheme: DialogTheme(
|
||||||
elevation: 15,
|
elevation: 15,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -545,9 +555,9 @@ class MyTheme {
|
|||||||
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
|
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void changeDarkMode(ThemeMode mode) async {
|
static Future<void> changeDarkMode(ThemeMode mode) async {
|
||||||
Get.changeThemeMode(mode);
|
Get.changeThemeMode(mode);
|
||||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
|
||||||
if (mode == ThemeMode.system) {
|
if (mode == ThemeMode.system) {
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
||||||
@@ -555,7 +565,7 @@ class MyTheme {
|
|||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||||
}
|
}
|
||||||
await bind.mainChangeTheme(dark: mode.toShortString());
|
if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
|
||||||
// Synchronize the window theme of the system.
|
// Synchronize the window theme of the system.
|
||||||
updateSystemWindowTheme();
|
updateSystemWindowTheme();
|
||||||
}
|
}
|
||||||
@@ -671,10 +681,12 @@ closeConnection({String? id}) {
|
|||||||
overlays: SystemUiOverlay.values);
|
overlays: SystemUiOverlay.values);
|
||||||
gFFI.chatModel.hideChatOverlay();
|
gFFI.chatModel.hideChatOverlay();
|
||||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||||
|
stateGlobal.isInMainPage = true;
|
||||||
}();
|
}();
|
||||||
} else {
|
} else {
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||||
|
stateGlobal.isInMainPage = true;
|
||||||
} else {
|
} else {
|
||||||
final controller = Get.find<DesktopTabController>();
|
final controller = Get.find<DesktopTabController>();
|
||||||
controller.closeBy(id);
|
controller.closeBy(id);
|
||||||
@@ -1162,33 +1174,21 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (reconnect != null && title == "Connection Error") {
|
if (reconnect != null &&
|
||||||
|
title == "Connection Error" &&
|
||||||
|
reconnectTimeout != null) {
|
||||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||||
final enabled = true.obs;
|
final enabled = true.obs;
|
||||||
final button = reconnectTimeout != null
|
final button = Obx(() => _ReconnectCountDownButton(
|
||||||
? Obx(() => _ReconnectCountDownButton(
|
second: reconnectTimeout,
|
||||||
second: reconnectTimeout,
|
onPressed: enabled.isTrue
|
||||||
onPressed: enabled.isTrue
|
? () {
|
||||||
? () {
|
// Disable the button
|
||||||
// Disable the button
|
enabled.value = false;
|
||||||
enabled.value = false;
|
reconnect(dialogManager, sessionId, false);
|
||||||
reconnect(dialogManager, sessionId, false);
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
));
|
||||||
))
|
|
||||||
: Obx(
|
|
||||||
() => dialogButton(
|
|
||||||
'Reconnect',
|
|
||||||
isOutline: true,
|
|
||||||
onPressed: enabled.isTrue
|
|
||||||
? () {
|
|
||||||
// Disable the button
|
|
||||||
enabled.value = false;
|
|
||||||
reconnect(dialogManager, sessionId, false);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
buttons.insert(0, button);
|
buttons.insert(0, button);
|
||||||
}
|
}
|
||||||
if (link.isNotEmpty) {
|
if (link.isNotEmpty) {
|
||||||
@@ -2026,6 +2026,8 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var webInitialLink = "";
|
||||||
|
|
||||||
/// Initialize uni links for macos/windows
|
/// Initialize uni links for macos/windows
|
||||||
///
|
///
|
||||||
/// [Availability]
|
/// [Availability]
|
||||||
@@ -2042,7 +2044,12 @@ Future<bool> initUniLinks() async {
|
|||||||
if (initialLink == null || initialLink.isEmpty) {
|
if (initialLink == null || initialLink.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return handleUriLink(uriString: initialLink);
|
if (isWeb) {
|
||||||
|
webInitialLink = initialLink;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return handleUriLink(uriString: initialLink);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugPrintStack(label: "$err");
|
debugPrintStack(label: "$err");
|
||||||
return false;
|
return false;
|
||||||
@@ -2055,7 +2062,7 @@ Future<bool> initUniLinks() async {
|
|||||||
///
|
///
|
||||||
/// Returns a [StreamSubscription] which can listen the uni links.
|
/// Returns a [StreamSubscription] which can listen the uni links.
|
||||||
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
|
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
|
||||||
if (isLinux) {
|
if (isLinux || isWeb) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2285,16 +2292,19 @@ connectMainDesktop(String id,
|
|||||||
required bool isRDP,
|
required bool isRDP,
|
||||||
bool? forceRelay,
|
bool? forceRelay,
|
||||||
String? password,
|
String? password,
|
||||||
|
String? connToken,
|
||||||
bool? isSharedPassword}) async {
|
bool? isSharedPassword}) async {
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
await rustDeskWinManager.newFileTransfer(id,
|
await rustDeskWinManager.newFileTransfer(id,
|
||||||
password: password,
|
password: password,
|
||||||
isSharedPassword: isSharedPassword,
|
isSharedPassword: isSharedPassword,
|
||||||
|
connToken: connToken,
|
||||||
forceRelay: forceRelay);
|
forceRelay: forceRelay);
|
||||||
} else if (isTcpTunneling || isRDP) {
|
} else if (isTcpTunneling || isRDP) {
|
||||||
await rustDeskWinManager.newPortForward(id, isRDP,
|
await rustDeskWinManager.newPortForward(id, isRDP,
|
||||||
password: password,
|
password: password,
|
||||||
isSharedPassword: isSharedPassword,
|
isSharedPassword: isSharedPassword,
|
||||||
|
connToken: connToken,
|
||||||
forceRelay: forceRelay);
|
forceRelay: forceRelay);
|
||||||
} else {
|
} else {
|
||||||
await rustDeskWinManager.newRemoteDesktop(id,
|
await rustDeskWinManager.newRemoteDesktop(id,
|
||||||
@@ -2314,6 +2324,7 @@ connect(BuildContext context, String id,
|
|||||||
bool isRDP = false,
|
bool isRDP = false,
|
||||||
bool forceRelay = false,
|
bool forceRelay = false,
|
||||||
String? password,
|
String? password,
|
||||||
|
String? connToken,
|
||||||
bool? isSharedPassword}) async {
|
bool? isSharedPassword}) async {
|
||||||
if (id == '') return;
|
if (id == '') return;
|
||||||
if (!isDesktop || desktopType == DesktopType.main) {
|
if (!isDesktop || desktopType == DesktopType.main) {
|
||||||
@@ -2355,24 +2366,40 @@ connect(BuildContext context, String id,
|
|||||||
'password': password,
|
'password': password,
|
||||||
'isSharedPassword': isSharedPassword,
|
'isSharedPassword': isSharedPassword,
|
||||||
'forceRelay': forceRelay,
|
'forceRelay': forceRelay,
|
||||||
|
'connToken': connToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
if (isAndroid) {
|
||||||
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
return;
|
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Navigator.push(
|
if (isWeb) {
|
||||||
context,
|
Navigator.push(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (BuildContext context) => FileManagerPage(
|
MaterialPageRoute(
|
||||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
builder: (BuildContext context) =>
|
||||||
),
|
desktop_file_manager.FileManagerPage(
|
||||||
);
|
id: id,
|
||||||
|
password: password,
|
||||||
|
isSharedPassword: isSharedPassword),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (BuildContext context) => FileManagerPage(
|
||||||
|
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isWebDesktop) {
|
if (isWeb) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -2396,6 +2423,7 @@ connect(BuildContext context, String id,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stateGlobal.isInMainPage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||||
@@ -3145,9 +3173,13 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
|
|||||||
|
|
||||||
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
||||||
String? text) {
|
String? text) {
|
||||||
|
text = text?.trim();
|
||||||
if (text != null && text.isNotEmpty) {
|
if (text != null && text.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final sc = ServerConfig.decode(text);
|
final sc = ServerConfig.decode(text);
|
||||||
|
if (isWeb || isIOS) {
|
||||||
|
sc.relayServer = '';
|
||||||
|
}
|
||||||
if (sc.idServer.isNotEmpty) {
|
if (sc.idServer.isNotEmpty) {
|
||||||
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
||||||
success.then((value) {
|
success.then((value) {
|
||||||
@@ -3587,3 +3619,7 @@ List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
|
|||||||
SubWindowResizeEdge.topRight,
|
SubWindowResizeEdge.topRight,
|
||||||
]
|
]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
void earlyAssert() {
|
||||||
|
assert('\1' == '1');
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||||
import 'package:flutter_hbb/models/ab_model.dart';
|
import 'package:flutter_hbb/models/ab_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -61,15 +62,16 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
retry: null, // remove retry
|
retry: null, // remove retry
|
||||||
close: () => gFFI.abModel.currentAbPushError.value = ''),
|
close: () => gFFI.abModel.currentAbPushError.value = ''),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: (isDesktop || isWebDesktop)
|
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||||
? _buildAddressBookDesktop()
|
? _buildAddressBookPortrait()
|
||||||
: _buildAddressBookMobile())
|
: _buildAddressBookLandscape()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildAddressBookDesktop() {
|
Widget _buildAddressBookLandscape() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -106,7 +108,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAddressBookMobile() {
|
Widget _buildAddressBookPortrait() {
|
||||||
const padding = 8.0;
|
const padding = 8.0;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -239,14 +241,15 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
customButton: Container(
|
customButton: Obx(() => Container(
|
||||||
height: isDesktop ? 48 : 40,
|
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
child:
|
||||||
Icon(Icons.arrow_drop_down),
|
buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||||
]),
|
Icon(Icons.arrow_drop_down),
|
||||||
),
|
]),
|
||||||
|
)),
|
||||||
underline: Container(
|
underline: Container(
|
||||||
height: 0.7,
|
height: 0.7,
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
@@ -335,8 +338,8 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
showActionMenu: editPermission);
|
showActionMenu: editPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
final gridView = DynamicGridView.builder(
|
gridView(bool isPortrait) => DynamicGridView.builder(
|
||||||
shrinkWrap: isMobile,
|
shrinkWrap: isPortrait,
|
||||||
gridDelegate: SliverGridDelegateWithWrapping(),
|
gridDelegate: SliverGridDelegateWithWrapping(),
|
||||||
itemCount: tags.length,
|
itemCount: tags.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
@@ -344,9 +347,9 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return tagBuilder(e);
|
return tagBuilder(e);
|
||||||
});
|
});
|
||||||
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||||
return (isDesktop || isWebDesktop)
|
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||||
? gridView
|
? gridView(false)
|
||||||
: LimitedBox(maxHeight: maxHeight, child: gridView);
|
: LimitedBox(maxHeight: maxHeight, child: gridView(true)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +359,6 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: AddressBookPeersView(
|
child: AddressBookPeersView(
|
||||||
menuPadding: widget.menuPadding,
|
menuPadding: widget.menuPadding,
|
||||||
getInitPeers: () => gFFI.abModel.currentAbPeers,
|
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -506,20 +508,21 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
double marginBottom = 4;
|
double marginBottom = 4;
|
||||||
|
|
||||||
row({required Widget lable, required Widget input}) {
|
row({required Widget lable, required Widget input}) {
|
||||||
return Row(
|
makeChild(bool isPortrait) => Row(
|
||||||
children: [
|
children: [
|
||||||
!isMobile
|
!isPortrait
|
||||||
? ConstrainedBox(
|
? ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 100),
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
child: lable.marginOnly(right: 10))
|
child: lable.marginOnly(right: 10))
|
||||||
: SizedBox.shrink(),
|
: SizedBox.shrink(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 200),
|
constraints: const BoxConstraints(minWidth: 200),
|
||||||
child: input),
|
child: input),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: !isMobile ? 8 : 0);
|
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||||
|
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
@@ -542,23 +545,28 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
input: TextField(
|
input: Obx(() => TextField(
|
||||||
controller: idController,
|
controller: idController,
|
||||||
inputFormatters: [IDTextInputFormatter()],
|
inputFormatters: [IDTextInputFormatter()],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: !isMobile ? null : translate('ID'),
|
labelText: stateGlobal.isPortrait.isFalse
|
||||||
errorText: errorMsg,
|
? null
|
||||||
errorMaxLines: 5),
|
: translate('ID'),
|
||||||
)),
|
errorText: errorMsg,
|
||||||
|
errorMaxLines: 5),
|
||||||
|
))),
|
||||||
row(
|
row(
|
||||||
lable: Text(
|
lable: Text(
|
||||||
translate('Alias'),
|
translate('Alias'),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
input: TextField(
|
input: Obx(() => TextField(
|
||||||
controller: aliasController,
|
controller: aliasController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: !isMobile ? null : translate('Alias'),
|
labelText: stateGlobal.isPortrait.isFalse
|
||||||
|
? null
|
||||||
|
: translate('Alias'),
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
if (isCurrentAbShared)
|
if (isCurrentAbShared)
|
||||||
@@ -567,22 +575,26 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
translate('Password'),
|
translate('Password'),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
input: TextField(
|
input: Obx(
|
||||||
controller: passwordController,
|
() => TextField(
|
||||||
obscureText: !passwordVisible,
|
controller: passwordController,
|
||||||
decoration: InputDecoration(
|
obscureText: !passwordVisible,
|
||||||
labelText: !isMobile ? null : translate('Password'),
|
decoration: InputDecoration(
|
||||||
suffixIcon: IconButton(
|
labelText: stateGlobal.isPortrait.isFalse
|
||||||
icon: Icon(
|
? null
|
||||||
passwordVisible
|
: translate('Password'),
|
||||||
? Icons.visibility
|
suffixIcon: IconButton(
|
||||||
: Icons.visibility_off,
|
icon: Icon(
|
||||||
color: MyTheme.lightTheme.primaryColor),
|
passwordVisible
|
||||||
onPressed: () {
|
? Icons.visibility
|
||||||
setState(() {
|
: Icons.visibility_off,
|
||||||
passwordVisible = !passwordVisible;
|
color: MyTheme.lightTheme.primaryColor),
|
||||||
});
|
onPressed: () {
|
||||||
},
|
setState(() {
|
||||||
|
passwordVisible = !passwordVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
|
|||||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||||
.toList();
|
.toList();
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: isMobile
|
message: !(isDesktop || isWebDesktop)
|
||||||
? ''
|
? ''
|
||||||
: widget.peer.tags.isNotEmpty
|
: widget.peer.tags.isNotEmpty
|
||||||
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
|
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
|
||||||
|
|||||||
38
flutter/lib/common/widgets/connection_page_title.dart
Normal file
38
flutter/lib/common/widgets/connection_page_title.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
|
||||||
|
Widget getConnectionPageTitle(BuildContext context, bool isWeb) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AutoSizeText(
|
||||||
|
translate('Control Remote Desktop'),
|
||||||
|
maxLines: 1,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleLarge
|
||||||
|
?.merge(TextStyle(height: 1)),
|
||||||
|
).marginOnly(right: 4),
|
||||||
|
Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 300),
|
||||||
|
message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"),
|
||||||
|
child: Icon(
|
||||||
|
Icons.help_outline_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleLarge
|
||||||
|
?.color
|
||||||
|
?.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
|
|||||||
String get name => translate('uppercase');
|
String get name => translate('uppercase');
|
||||||
@override
|
@override
|
||||||
bool validate(String value) {
|
bool validate(String value) {
|
||||||
return value.contains(RegExp(r'[A-Z]'));
|
return value.runes.any((int rune) {
|
||||||
|
var character = String.fromCharCode(rune);
|
||||||
|
return character.toUpperCase() == character &&
|
||||||
|
character.toLowerCase() != character;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool validate(String value) {
|
bool validate(String value) {
|
||||||
return value.contains(RegExp(r'[a-z]'));
|
return value.runes.any((int rune) {
|
||||||
|
var character = String.fromCharCode(rune);
|
||||||
|
return character.toLowerCase() == character &&
|
||||||
|
character.toUpperCase() != character;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
|||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
|
||||||
@@ -380,6 +381,7 @@ class DialogTextField extends StatelessWidget {
|
|||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
final TextInputType? keyboardType;
|
final TextInputType? keyboardType;
|
||||||
final List<TextInputFormatter>? inputFormatters;
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
final int? maxLength;
|
||||||
|
|
||||||
static const kUsernameTitle = 'Username';
|
static const kUsernameTitle = 'Username';
|
||||||
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
||||||
@@ -397,6 +399,7 @@ class DialogTextField extends StatelessWidget {
|
|||||||
this.hintText,
|
this.hintText,
|
||||||
this.keyboardType,
|
this.keyboardType,
|
||||||
this.inputFormatters,
|
this.inputFormatters,
|
||||||
|
this.maxLength,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.controller})
|
required this.controller})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -423,6 +426,7 @@ class DialogTextField extends StatelessWidget {
|
|||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
|
maxLength: maxLength,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -680,6 +684,7 @@ class PasswordWidget extends StatefulWidget {
|
|||||||
this.hintText,
|
this.hintText,
|
||||||
this.errorText,
|
this.errorText,
|
||||||
this.title,
|
this.title,
|
||||||
|
this.maxLength,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
@@ -688,6 +693,7 @@ class PasswordWidget extends StatefulWidget {
|
|||||||
final String? hintText;
|
final String? hintText;
|
||||||
final String? errorText;
|
final String? errorText;
|
||||||
final String? title;
|
final String? title;
|
||||||
|
final int? maxLength;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PasswordWidget> createState() => _PasswordWidgetState();
|
State<PasswordWidget> createState() => _PasswordWidgetState();
|
||||||
@@ -750,6 +756,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
|||||||
obscureText: !_passwordVisible,
|
obscureText: !_passwordVisible,
|
||||||
errorText: widget.errorText,
|
errorText: widget.errorText,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
|
maxLength: widget.maxLength,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1123,7 +1130,7 @@ void showRequestElevationDialog(
|
|||||||
errorText: errPwd.isEmpty ? null : errPwd.value,
|
errorText: errPwd.isEmpty ? null : errPwd.value,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
|
).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
|
||||||
).marginOnly(top: 10),
|
).marginOnly(top: 10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -2244,6 +2251,7 @@ void changeUnlockPinDialog(String oldPin, Function() callback) {
|
|||||||
final confirmController = TextEditingController(text: oldPin);
|
final confirmController = TextEditingController(text: oldPin);
|
||||||
String? pinErrorText;
|
String? pinErrorText;
|
||||||
String? confirmationErrorText;
|
String? confirmationErrorText;
|
||||||
|
final maxLength = bind.mainMaxEncryptLen();
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
submit() async {
|
submit() async {
|
||||||
pinErrorText = null;
|
pinErrorText = null;
|
||||||
@@ -2277,12 +2285,14 @@ void changeUnlockPinDialog(String oldPin, Function() callback) {
|
|||||||
controller: pinController,
|
controller: pinController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
errorText: pinErrorText,
|
errorText: pinErrorText,
|
||||||
|
maxLength: maxLength,
|
||||||
),
|
),
|
||||||
DialogTextField(
|
DialogTextField(
|
||||||
title: translate('Confirmation'),
|
title: translate('Confirmation'),
|
||||||
controller: confirmController,
|
controller: confirmController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
errorText: confirmationErrorText,
|
errorText: confirmationErrorText,
|
||||||
|
maxLength: maxLength,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: 12),
|
).marginOnly(bottom: 12),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/login.dart';
|
import 'package:flutter_hbb/common/widgets/login.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
@@ -45,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
retry: null,
|
retry: null,
|
||||||
close: () => gFFI.groupModel.groupLoadError.value = ''),
|
close: () => gFFI.groupModel.groupLoadError.value = ''),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: (isDesktop || isWebDesktop)
|
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||||
? _buildDesktop()
|
? _buildPortrait()
|
||||||
: _buildMobile())
|
: _buildLandscape())),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDesktop() {
|
Widget _buildLandscape() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -82,14 +83,14 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: MyGroupPeerView(
|
child: MyGroupPeerView(
|
||||||
menuPadding: widget.menuPadding,
|
menuPadding: widget.menuPadding,
|
||||||
getInitPeers: () => gFFI.groupModel.peers)),
|
)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMobile() {
|
Widget _buildPortrait() {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -114,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: MyGroupPeerView(
|
child: MyGroupPeerView(
|
||||||
menuPadding: widget.menuPadding,
|
menuPadding: widget.menuPadding,
|
||||||
getInitPeers: () => gFFI.groupModel.peers)),
|
)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -159,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
final listView = ListView.builder(
|
listView(bool isPortrait) => ListView.builder(
|
||||||
shrinkWrap: isMobile,
|
shrinkWrap: isPortrait,
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
||||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||||
return (isDesktop || isWebDesktop)
|
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||||
? listView
|
? listView(false)
|
||||||
: LimitedBox(maxHeight: maxHeight, child: listView);
|
: LimitedBox(maxHeight: maxHeight, child: listView(true)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -53,42 +54,44 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
if (isDesktop || isWebDesktop) {
|
return Obx(() =>
|
||||||
return _buildDesktop();
|
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
|
||||||
} else {
|
|
||||||
return _buildMobile();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMobile() {
|
Widget gestureDetector({required Widget child}) {
|
||||||
final peer = super.widget.peer;
|
|
||||||
final PeerTabModel peerTabModel = Provider.of(context);
|
final PeerTabModel peerTabModel = Provider.of(context);
|
||||||
|
final peer = super.widget.peer;
|
||||||
|
return GestureDetector(
|
||||||
|
onDoubleTap: peerTabModel.multiSelectionMode
|
||||||
|
? null
|
||||||
|
: () => widget.connect(context, peer.id),
|
||||||
|
onTap: () {
|
||||||
|
if (peerTabModel.multiSelectionMode) {
|
||||||
|
peerTabModel.select(peer);
|
||||||
|
} else {
|
||||||
|
if (isMobile) {
|
||||||
|
widget.connect(context, peer.id);
|
||||||
|
} else {
|
||||||
|
peerTabModel.select(peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () => peerTabModel.select(peer),
|
||||||
|
child: child);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPortrait() {
|
||||||
|
final peer = super.widget.peer;
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 2),
|
margin: EdgeInsets.symmetric(horizontal: 2),
|
||||||
child: GestureDetector(
|
child: gestureDetector(
|
||||||
onTap: () {
|
|
||||||
if (peerTabModel.multiSelectionMode) {
|
|
||||||
peerTabModel.select(peer);
|
|
||||||
} else {
|
|
||||||
if (!isWebDesktop) {
|
|
||||||
connectInPeerTab(context, peer, widget.tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDoubleTap: isWebDesktop
|
|
||||||
? () => connectInPeerTab(context, peer, widget.tab)
|
|
||||||
: null,
|
|
||||||
onLongPress: () {
|
|
||||||
peerTabModel.select(peer);
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
||||||
child: _buildPeerTile(context, peer, null)),
|
child: _buildPeerTile(context, peer, null)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDesktop() {
|
Widget _buildLandscape() {
|
||||||
final PeerTabModel peerTabModel = Provider.of(context);
|
|
||||||
final peer = super.widget.peer;
|
final peer = super.widget.peer;
|
||||||
var deco = Rx<BoxDecoration?>(
|
var deco = Rx<BoxDecoration?>(
|
||||||
BoxDecoration(
|
BoxDecoration(
|
||||||
@@ -117,36 +120,27 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: GestureDetector(
|
child: gestureDetector(
|
||||||
onDoubleTap:
|
|
||||||
peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
|
|
||||||
? null
|
|
||||||
: () => widget.connect(context, peer.id),
|
|
||||||
onTap: () => peerTabModel.select(peer),
|
|
||||||
onLongPress: () => peerTabModel.select(peer),
|
|
||||||
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
child: Obx(() => peerCardUiType.value == PeerUiType.grid
|
||||||
? _buildPeerCard(context, peer, deco)
|
? _buildPeerCard(context, peer, deco)
|
||||||
: _buildPeerTile(context, peer, deco))),
|
: _buildPeerTile(context, peer, deco))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPeerTile(
|
makeChild(bool isPortrait, Peer peer) {
|
||||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
|
||||||
hideUsernameOnCard ??=
|
|
||||||
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
|
||||||
final name = hideUsernameOnCard == true
|
final name = hideUsernameOnCard == true
|
||||||
? peer.hostname
|
? peer.hostname
|
||||||
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||||
final greyStyle = TextStyle(
|
final greyStyle = TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||||
final child = Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||||
borderRadius: isMobile
|
borderRadius: isPortrait
|
||||||
? BorderRadius.circular(_tileRadius)
|
? BorderRadius.circular(_tileRadius)
|
||||||
: BorderRadius.only(
|
: BorderRadius.only(
|
||||||
topLeft: Radius.circular(_tileRadius),
|
topLeft: Radius.circular(_tileRadius),
|
||||||
@@ -154,11 +148,11 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
width: isMobile ? 50 : 42,
|
width: isPortrait ? 50 : 42,
|
||||||
height: isMobile ? 50 : null,
|
height: isPortrait ? 50 : null,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
|
||||||
.paddingAll(6),
|
.paddingAll(6),
|
||||||
if (_shouldBuildPasswordIcon(peer))
|
if (_shouldBuildPasswordIcon(peer))
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -183,19 +177,19 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
getOnline(isMobile ? 4 : 8, peer.online),
|
getOnline(isPortrait ? 4 : 8, peer.online),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
)),
|
)),
|
||||||
]).marginOnly(top: isMobile ? 0 : 2),
|
]).marginOnly(top: isPortrait ? 0 : 2),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
name,
|
name,
|
||||||
style: isMobile ? null : greyStyle,
|
style: isPortrait ? null : greyStyle,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -203,41 +197,47 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
],
|
],
|
||||||
).marginOnly(top: 2),
|
).marginOnly(top: 2),
|
||||||
),
|
),
|
||||||
isMobile
|
isPortrait
|
||||||
? checkBoxOrActionMoreMobile(peer)
|
? checkBoxOrActionMorePortrait(peer)
|
||||||
: checkBoxOrActionMoreDesktop(peer, isTile: true),
|
: checkBoxOrActionMoreLandscape(peer, isTile: true),
|
||||||
],
|
],
|
||||||
).paddingOnly(left: 10.0, top: 3.0),
|
).paddingOnly(left: 10.0, top: 3.0),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPeerTile(
|
||||||
|
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||||
|
hideUsernameOnCard ??=
|
||||||
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||||
final colors = _frontN(peer.tags, 25)
|
final colors = _frontN(peer.tags, 25)
|
||||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||||
.toList();
|
.toList();
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: isMobile
|
message: !(isDesktop || isWebDesktop)
|
||||||
? ''
|
? ''
|
||||||
: peer.tags.isNotEmpty
|
: peer.tags.isNotEmpty
|
||||||
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
||||||
: '',
|
: '',
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
deco == null
|
Obx(
|
||||||
? child
|
() => deco == null
|
||||||
: Obx(
|
? makeChild(stateGlobal.isPortrait.isTrue, peer)
|
||||||
() => Container(
|
: Container(
|
||||||
foregroundDecoration: deco.value,
|
foregroundDecoration: deco.value,
|
||||||
child: child,
|
child: makeChild(stateGlobal.isPortrait.isTrue, peer),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (colors.isNotEmpty)
|
if (colors.isNotEmpty)
|
||||||
Positioned(
|
Obx(() => Positioned(
|
||||||
top: 2,
|
top: 2,
|
||||||
right: isMobile ? 20 : 10,
|
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: TagPainter(radius: 3, colors: colors),
|
painter: TagPainter(radius: 3, colors: colors),
|
||||||
),
|
),
|
||||||
)
|
))
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -253,6 +253,9 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
|
// to-do: memory leak here, more investigation needed.
|
||||||
|
// Continious rebuilds of `Obx()` will cause memory leak here.
|
||||||
|
// The simple demo does not have this issue.
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => Container(
|
() => Container(
|
||||||
foregroundDecoration: deco.value,
|
foregroundDecoration: deco.value,
|
||||||
@@ -316,7 +319,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
)),
|
)),
|
||||||
]).paddingSymmetric(vertical: 8)),
|
]).paddingSymmetric(vertical: 8)),
|
||||||
checkBoxOrActionMoreDesktop(peer, isTile: false),
|
checkBoxOrActionMoreLandscape(peer, isTile: false),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 12.0),
|
).paddingSymmetric(horizontal: 12.0),
|
||||||
)
|
)
|
||||||
@@ -362,7 +365,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget checkBoxOrActionMoreMobile(Peer peer) {
|
Widget checkBoxOrActionMorePortrait(Peer peer) {
|
||||||
final PeerTabModel peerTabModel = Provider.of(context);
|
final PeerTabModel peerTabModel = Provider.of(context);
|
||||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||||
if (peerTabModel.multiSelectionMode) {
|
if (peerTabModel.multiSelectionMode) {
|
||||||
@@ -390,7 +393,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) {
|
Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
|
||||||
final PeerTabModel peerTabModel = Provider.of(context);
|
final PeerTabModel peerTabModel = Provider.of(context);
|
||||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||||
if (peerTabModel.multiSelectionMode) {
|
if (peerTabModel.multiSelectionMode) {
|
||||||
@@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
|
|
||||||
final List favs = (await bind.mainGetFav()).toList();
|
final List favs = (await bind.mainGetFav()).toList();
|
||||||
@@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||||
menuItems.add(_tcpTunnelingAction(context));
|
menuItems.add(_tcpTunnelingAction(context));
|
||||||
@@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
|
|
||||||
final List favs = (await bind.mainGetFav()).toList();
|
final List favs = (await bind.mainGetFav()).toList();
|
||||||
@@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||||
menuItems.add(_tcpTunnelingAction(context));
|
menuItems.add(_tcpTunnelingAction(context));
|
||||||
@@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
|||||||
BuildContext context) async {
|
BuildContext context) async {
|
||||||
final List<MenuEntryBase<String>> menuItems = [
|
final List<MenuEntryBase<String>> menuItems = [
|
||||||
_connectAction(context),
|
_connectAction(context),
|
||||||
if (!isWeb) _transferFileAction(context),
|
_transferFileAction(context),
|
||||||
];
|
];
|
||||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||||
menuItems.add(_tcpTunnelingAction(context));
|
menuItems.add(_tcpTunnelingAction(context));
|
||||||
@@ -1203,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _rdpDialog(String id) async {
|
void _rdpDialog(String id) async {
|
||||||
|
final maxLength = bind.mainMaxEncryptLen();
|
||||||
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
|
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
|
||||||
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
|
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
|
||||||
final portController = TextEditingController(text: port);
|
final portController = TextEditingController(text: port);
|
||||||
@@ -1257,54 +1261,54 @@ void _rdpDialog(String id) async {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||||
Row(
|
Obx(() => Row(
|
||||||
children: [
|
children: [
|
||||||
(isDesktop || isWebDesktop)
|
stateGlobal.isPortrait.isFalse
|
||||||
? ConstrainedBox(
|
? ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${translate('Username')}:",
|
"${translate('Username')}:",
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
).marginOnly(right: 10))
|
).marginOnly(right: 10))
|
||||||
: SizedBox.shrink(),
|
: SizedBox.shrink(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: (isDesktop || isWebDesktop)
|
|
||||||
? null
|
|
||||||
: translate('Username')),
|
|
||||||
controller: userController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
(isDesktop || isWebDesktop)
|
|
||||||
? ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
|
||||||
child: Text(
|
|
||||||
"${translate('Password')}:",
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
).marginOnly(right: 10))
|
|
||||||
: SizedBox.shrink(),
|
|
||||||
Expanded(
|
|
||||||
child: Obx(() => TextField(
|
|
||||||
obscureText: secure.value,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: (isDesktop || isWebDesktop)
|
labelText:
|
||||||
? null
|
isDesktop ? null : translate('Username')),
|
||||||
: translate('Password'),
|
controller: userController,
|
||||||
suffixIcon: IconButton(
|
),
|
||||||
onPressed: () => secure.value = !secure.value,
|
),
|
||||||
icon: Icon(secure.value
|
],
|
||||||
? Icons.visibility_off
|
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||||
: Icons.visibility))),
|
Obx(() => Row(
|
||||||
controller: passwordController,
|
children: [
|
||||||
)),
|
stateGlobal.isPortrait.isFalse
|
||||||
),
|
? ConstrainedBox(
|
||||||
],
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
)
|
child: Text(
|
||||||
|
"${translate('Password')}:",
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
).marginOnly(right: 10))
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() => TextField(
|
||||||
|
obscureText: secure.value,
|
||||||
|
maxLength: maxLength,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText:
|
||||||
|
isDesktop ? null : translate('Password'),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: () =>
|
||||||
|
secure.value = !secure.value,
|
||||||
|
icon: Icon(secure.value
|
||||||
|
? Icons.visibility_off
|
||||||
|
: Icons.visibility))),
|
||||||
|
controller: passwordController,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
|
|||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -107,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
final model = Provider.of<PeerTabModel>(context);
|
||||||
Widget selectionWrap(Widget widget) {
|
Widget selectionWrap(Widget widget) {
|
||||||
return model.multiSelectionMode ? createMultiSelectionBar() : widget;
|
return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
textBaseline: TextBaseline.ideographic,
|
textBaseline: TextBaseline.ideographic,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
Obx(() => SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: (isDesktop || isWebDesktop)
|
padding: stateGlobal.isPortrait.isTrue
|
||||||
? null
|
? EdgeInsets.symmetric(horizontal: 2)
|
||||||
: EdgeInsets.symmetric(horizontal: 2),
|
: null,
|
||||||
child: selectionWrap(Row(
|
child: selectionWrap(Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child: visibleContextMenuListener(
|
||||||
visibleContextMenuListener(_createSwitchBar(context))),
|
_createSwitchBar(context))),
|
||||||
if (isMobile)
|
if (stateGlobal.isPortrait.isTrue)
|
||||||
..._mobileRightActions(context)
|
..._portraitRightActions(context)
|
||||||
else
|
else
|
||||||
..._desktopRightActions(context)
|
..._landscapeRightActions(context)
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
|
).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
|
||||||
_createPeersView(),
|
_createPeersView(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -299,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget visibleContextMenuListener(Widget child) {
|
Widget visibleContextMenuListener(Widget child) {
|
||||||
if (isMobile) {
|
if (!(isDesktop || isWebDesktop)) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onLongPressDown: (e) {
|
onLongPressDown: (e) {
|
||||||
final x = e.globalPosition.dx;
|
final x = e.globalPosition.dx;
|
||||||
@@ -361,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget createMultiSelectionBar() {
|
Widget createMultiSelectionBar(PeerTabModel model) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -380,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
selectionCount(model.selectedPeers.length),
|
selectionCount(model.selectedPeers.length),
|
||||||
selectAll(),
|
selectAll(model),
|
||||||
closeSelection(),
|
closeSelection(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -456,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
showToast(translate('Successful'));
|
showToast(translate('Successful'));
|
||||||
},
|
},
|
||||||
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
|
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
|
||||||
).marginOnly(left: isMobile ? 11 : 6),
|
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
model.setMultiSelectionMode(false);
|
model.setMultiSelectionMode(false);
|
||||||
},
|
},
|
||||||
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
|
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
|
||||||
).marginOnly(left: isMobile ? 11 : 6),
|
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,7 +500,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Icon(Icons.tag))
|
child: Icon(Icons.tag))
|
||||||
.marginOnly(left: isMobile ? 11 : 6),
|
.marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget selectAll() {
|
Widget selectAll(PeerTabModel model) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
|
||||||
return Offstage(
|
return Offstage(
|
||||||
offstage:
|
offstage:
|
||||||
model.selectedPeers.length >= model.currentTabCachedPeers.length,
|
model.selectedPeers.length >= model.currentTabCachedPeers.length,
|
||||||
@@ -556,10 +555,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _desktopRightActions(BuildContext context) {
|
List<Widget> _landscapeRightActions(BuildContext context) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
final model = Provider.of<PeerTabModel>(context);
|
||||||
return [
|
return [
|
||||||
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
|
const PeerSearchBar().marginOnly(right: 13),
|
||||||
_createRefresh(
|
_createRefresh(
|
||||||
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
||||||
_createRefresh(
|
_createRefresh(
|
||||||
@@ -580,7 +579,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _mobileRightActions(BuildContext context) {
|
List<Widget> _portraitRightActions(BuildContext context) {
|
||||||
final model = Provider.of<PeerTabModel>(context);
|
final model = Provider.of<PeerTabModel>(context);
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
|
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
|
||||||
@@ -701,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
|||||||
baseOffset: 0,
|
baseOffset: 0,
|
||||||
extentOffset: peerSearchTextController.value.text.length);
|
extentOffset: peerSearchTextController.value.text.length);
|
||||||
});
|
});
|
||||||
return Container(
|
return Obx(() => Container(
|
||||||
width: isMobile ? 120 : 140,
|
width: stateGlobal.isPortrait.isTrue ? 120 : 140,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.background,
|
color: Theme.of(context).colorScheme.background,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Obx(() => Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -768,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||||
|
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
@@ -41,6 +43,14 @@ class LoadEvent {
|
|||||||
static const String group = 'load_group_peers';
|
static const String group = 'load_group_peers';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PeersModelName {
|
||||||
|
static const String recent = 'recent peer';
|
||||||
|
static const String favorite = 'fav peer';
|
||||||
|
static const String lan = 'discovered peer';
|
||||||
|
static const String addressBook = 'address book peer';
|
||||||
|
static const String group = 'group peer';
|
||||||
|
}
|
||||||
|
|
||||||
/// for peer search text, global obs value
|
/// for peer search text, global obs value
|
||||||
final peerSearchText = "".obs;
|
final peerSearchText = "".obs;
|
||||||
|
|
||||||
@@ -88,6 +98,7 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
var _lastChangeTime = DateTime.now();
|
var _lastChangeTime = DateTime.now();
|
||||||
var _lastQueryPeers = <String>{};
|
var _lastQueryPeers = <String>{};
|
||||||
var _lastQueryTime = DateTime.now();
|
var _lastQueryTime = DateTime.now();
|
||||||
|
var _lastWindowRestoreTime = DateTime.now();
|
||||||
var _queryCount = 0;
|
var _queryCount = 0;
|
||||||
var _exit = false;
|
var _exit = false;
|
||||||
bool _isActive = true;
|
bool _isActive = true;
|
||||||
@@ -116,11 +127,38 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
@override
|
@override
|
||||||
void onWindowFocus() {
|
void onWindowFocus() {
|
||||||
_queryCount = 0;
|
_queryCount = 0;
|
||||||
|
_isActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowBlur() {
|
||||||
|
// We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
|
||||||
|
// Maybe it's a bug of the window manager, but the source code seems to be correct.
|
||||||
|
//
|
||||||
|
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
|
||||||
|
// we need the following comparison to ensure that `_isActive` is true in the end.
|
||||||
|
if (isWindows &&
|
||||||
|
DateTime.now().difference(_lastWindowRestoreTime) <
|
||||||
|
const Duration(milliseconds: 300)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_queryCount = _maxQueryCount;
|
||||||
|
_isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowRestore() {
|
||||||
|
// Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
|
||||||
|
// But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
|
||||||
|
if (!isWindows) return;
|
||||||
|
_queryCount = 0;
|
||||||
|
_isActive = true;
|
||||||
|
_lastWindowRestoreTime = DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowMinimize() {
|
void onWindowMinimize() {
|
||||||
_queryCount = _maxQueryCount;
|
// Window minimize also triggers `onWindowBlur()`.
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is required for mobile.
|
// This function is required for mobile.
|
||||||
@@ -128,7 +166,7 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
super.didChangeAppLifecycleState(state);
|
super.didChangeAppLifecycleState(state);
|
||||||
if (isDesktop) return;
|
if (isDesktop || isWebDesktop) return;
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
_isActive = true;
|
_isActive = true;
|
||||||
_queryCount = 0;
|
_queryCount = 0;
|
||||||
@@ -139,8 +177,11 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<Peers>(
|
// We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
|
||||||
create: (context) => widget.peers,
|
// Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
|
||||||
|
// Simple demo can reproduce this issue.
|
||||||
|
return ChangeNotifierProvider<Peers>.value(
|
||||||
|
value: widget.peers,
|
||||||
child: Consumer<Peers>(builder: (context, peers, child) {
|
child: Consumer<Peers>(builder: (context, peers, child) {
|
||||||
if (peers.peers.isEmpty) {
|
if (peers.peers.isEmpty) {
|
||||||
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
||||||
@@ -194,7 +235,7 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
var peers = snapshot.data!;
|
var peers = snapshot.data!;
|
||||||
if (peers.length > 1000) peers = peers.sublist(0, 1000);
|
if (peers.length > 1000) peers = peers.sublist(0, 1000);
|
||||||
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
|
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
|
||||||
buildOnePeer(Peer peer) {
|
buildOnePeer(Peer peer, bool isPortrait) {
|
||||||
final visibilityChild = VisibilityDetector(
|
final visibilityChild = VisibilityDetector(
|
||||||
key: ValueKey(_cardId(peer.id)),
|
key: ValueKey(_cardId(peer.id)),
|
||||||
onVisibilityChanged: onVisibilityChanged,
|
onVisibilityChanged: onVisibilityChanged,
|
||||||
@@ -206,7 +247,7 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
// No need to listen the currentTab change event.
|
// No need to listen the currentTab change event.
|
||||||
// Because the currentTab change event will trigger the peers change event,
|
// Because the currentTab change event will trigger the peers change event,
|
||||||
// and the peers change event will trigger _buildPeersView().
|
// and the peers change event will trigger _buildPeersView().
|
||||||
return (isDesktop || isWebDesktop)
|
return !isPortrait
|
||||||
? Obx(() => peerCardUiType.value == PeerUiType.list
|
? Obx(() => peerCardUiType.value == PeerUiType.list
|
||||||
? Container(height: 45, child: visibilityChild)
|
? Container(height: 45, child: visibilityChild)
|
||||||
: peerCardUiType.value == PeerUiType.grid
|
: peerCardUiType.value == PeerUiType.grid
|
||||||
@@ -217,44 +258,45 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
: Container(child: visibilityChild);
|
: Container(child: visibilityChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Widget child;
|
// We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
|
||||||
if (isMobile) {
|
// Continious rebuilds of `ListView.builder` will cause memory leak.
|
||||||
child = ListView.builder(
|
// Simple demo can reproduce this issue.
|
||||||
itemCount: peers.length,
|
final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
|
||||||
itemBuilder: (BuildContext context, int index) {
|
? ListView.builder(
|
||||||
return buildOnePeer(peers[index]).marginOnly(
|
itemCount: peers.length,
|
||||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
itemBuilder: (BuildContext context, int index) {
|
||||||
},
|
return buildOnePeer(peers[index], true).marginOnly(
|
||||||
);
|
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||||
} else {
|
},
|
||||||
child = Obx(() => peerCardUiType.value == PeerUiType.list
|
)
|
||||||
? DesktopScrollWrapper(
|
: peerCardUiType.value == PeerUiType.list
|
||||||
scrollController: _scrollController,
|
? DesktopScrollWrapper(
|
||||||
child: ListView.builder(
|
scrollController: _scrollController,
|
||||||
controller: _scrollController,
|
child: ListView.builder(
|
||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
controller: _scrollController,
|
||||||
itemCount: peers.length,
|
physics: DraggableNeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemCount: peers.length,
|
||||||
return buildOnePeer(peers[index]).marginOnly(
|
itemBuilder: (BuildContext context, int index) {
|
||||||
right: space,
|
return buildOnePeer(peers[index], false)
|
||||||
top: index == 0 ? 0 : space / 2,
|
.marginOnly(
|
||||||
bottom: space / 2);
|
right: space,
|
||||||
}),
|
top: index == 0 ? 0 : space / 2,
|
||||||
)
|
bottom: space / 2);
|
||||||
: DesktopScrollWrapper(
|
}),
|
||||||
scrollController: _scrollController,
|
)
|
||||||
child: DynamicGridView.builder(
|
: DesktopScrollWrapper(
|
||||||
controller: _scrollController,
|
scrollController: _scrollController,
|
||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
child: DynamicGridView.builder(
|
||||||
gridDelegate: SliverGridDelegateWithWrapping(
|
controller: _scrollController,
|
||||||
mainAxisSpacing: space / 2,
|
physics: DraggableNeverScrollableScrollPhysics(),
|
||||||
crossAxisSpacing: space),
|
gridDelegate: SliverGridDelegateWithWrapping(
|
||||||
itemCount: peers.length,
|
mainAxisSpacing: space / 2,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
crossAxisSpacing: space),
|
||||||
return buildOnePeer(peers[index]);
|
itemCount: peers.length,
|
||||||
}),
|
itemBuilder: (BuildContext context, int index) {
|
||||||
));
|
return buildOnePeer(peers[index], false);
|
||||||
}
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
if (updateEvent == UpdateEvent.load) {
|
if (updateEvent == UpdateEvent.load) {
|
||||||
_curPeers.clear();
|
_curPeers.clear();
|
||||||
@@ -290,7 +332,12 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
_queryOnlines(false);
|
_queryOnlines(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_isActive && (_queryCount < _maxQueryCount || !p)) {
|
final skipIfIsWeb =
|
||||||
|
isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage);
|
||||||
|
final skipIfMobile =
|
||||||
|
(isAndroid || isIOS) && !stateGlobal.isInMainPage;
|
||||||
|
final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive;
|
||||||
|
if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) {
|
||||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||||
if (_curPeers.isNotEmpty) {
|
if (_curPeers.isNotEmpty) {
|
||||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
@@ -371,28 +418,39 @@ class _PeersViewState extends State<_PeersView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class BasePeersView extends StatelessWidget {
|
abstract class BasePeersView extends StatelessWidget {
|
||||||
final String name;
|
final PeerTabIndex peerTabIndex;
|
||||||
final String loadEvent;
|
|
||||||
final PeerFilter? peerFilter;
|
final PeerFilter? peerFilter;
|
||||||
final PeerCardBuilder peerCardBuilder;
|
final PeerCardBuilder peerCardBuilder;
|
||||||
final GetInitPeers? getInitPeers;
|
|
||||||
|
|
||||||
const BasePeersView({
|
const BasePeersView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.name,
|
required this.peerTabIndex,
|
||||||
required this.loadEvent,
|
|
||||||
this.peerFilter,
|
this.peerFilter,
|
||||||
required this.peerCardBuilder,
|
required this.peerCardBuilder,
|
||||||
required this.getInitPeers,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Peers peers;
|
||||||
|
switch (peerTabIndex) {
|
||||||
|
case PeerTabIndex.recent:
|
||||||
|
peers = gFFI.recentPeersModel;
|
||||||
|
break;
|
||||||
|
case PeerTabIndex.fav:
|
||||||
|
peers = gFFI.favoritePeersModel;
|
||||||
|
break;
|
||||||
|
case PeerTabIndex.lan:
|
||||||
|
peers = gFFI.lanPeersModel;
|
||||||
|
break;
|
||||||
|
case PeerTabIndex.ab:
|
||||||
|
peers = gFFI.abModel.peersModel;
|
||||||
|
break;
|
||||||
|
case PeerTabIndex.group:
|
||||||
|
peers = gFFI.groupModel.peersModel;
|
||||||
|
break;
|
||||||
|
}
|
||||||
return _PeersView(
|
return _PeersView(
|
||||||
peers:
|
peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
|
||||||
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
|
|
||||||
peerFilter: peerFilter,
|
|
||||||
peerCardBuilder: peerCardBuilder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,13 +459,11 @@ class RecentPeersView extends BasePeersView {
|
|||||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'recent peer',
|
peerTabIndex: PeerTabIndex.recent,
|
||||||
loadEvent: LoadEvent.recent,
|
|
||||||
peerCardBuilder: (Peer peer) => RecentPeerCard(
|
peerCardBuilder: (Peer peer) => RecentPeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -423,13 +479,11 @@ class FavoritePeersView extends BasePeersView {
|
|||||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'favorite peer',
|
peerTabIndex: PeerTabIndex.fav,
|
||||||
loadEvent: LoadEvent.favorite,
|
|
||||||
peerCardBuilder: (Peer peer) => FavoritePeerCard(
|
peerCardBuilder: (Peer peer) => FavoritePeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -445,13 +499,11 @@ class DiscoveredPeersView extends BasePeersView {
|
|||||||
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'discovered peer',
|
peerTabIndex: PeerTabIndex.lan,
|
||||||
loadEvent: LoadEvent.lan,
|
|
||||||
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
|
peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -464,21 +516,16 @@ class DiscoveredPeersView extends BasePeersView {
|
|||||||
|
|
||||||
class AddressBookPeersView extends BasePeersView {
|
class AddressBookPeersView extends BasePeersView {
|
||||||
AddressBookPeersView(
|
AddressBookPeersView(
|
||||||
{Key? key,
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
EdgeInsets? menuPadding,
|
|
||||||
ScrollController? scrollController,
|
|
||||||
required GetInitPeers getInitPeers})
|
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'address book peer',
|
peerTabIndex: PeerTabIndex.ab,
|
||||||
loadEvent: LoadEvent.addressBook,
|
|
||||||
peerFilter: (Peer peer) =>
|
peerFilter: (Peer peer) =>
|
||||||
_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
||||||
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
|
peerCardBuilder: (Peer peer) => AddressBookPeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: getInitPeers,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||||
@@ -505,20 +552,15 @@ class AddressBookPeersView extends BasePeersView {
|
|||||||
|
|
||||||
class MyGroupPeerView extends BasePeersView {
|
class MyGroupPeerView extends BasePeersView {
|
||||||
MyGroupPeerView(
|
MyGroupPeerView(
|
||||||
{Key? key,
|
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
|
||||||
EdgeInsets? menuPadding,
|
|
||||||
ScrollController? scrollController,
|
|
||||||
required GetInitPeers getInitPeers})
|
|
||||||
: super(
|
: super(
|
||||||
key: key,
|
key: key,
|
||||||
name: 'group peer',
|
peerTabIndex: PeerTabIndex.group,
|
||||||
loadEvent: LoadEvent.group,
|
|
||||||
peerFilter: filter,
|
peerFilter: filter,
|
||||||
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
|
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
|
||||||
peer: peer,
|
peer: peer,
|
||||||
menuPadding: menuPadding,
|
menuPadding: menuPadding,
|
||||||
),
|
),
|
||||||
getInitPeers: getInitPeers,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static bool filter(Peer peer) {
|
static bool filter(Peer peer) {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// https://github.com/flutter/flutter/issues/154053
|
||||||
|
final useRawKeyEvents = isLinux && !isWeb;
|
||||||
|
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
|
||||||
|
// while `Alt` and `Control` are seperated key events for en-US input method.
|
||||||
return FocusScope(
|
return FocusScope(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: Focus(
|
child: Focus(
|
||||||
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
|
|||||||
canRequestFocus: true,
|
canRequestFocus: true,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
onFocusChange: onFocusChange,
|
onFocusChange: onFocusChange,
|
||||||
onKey: (FocusNode data, RawKeyEvent e) =>
|
onKey: useRawKeyEvents
|
||||||
inputModel.handleRawKeyEvent(e),
|
? (FocusNode data, RawKeyEvent event) =>
|
||||||
|
inputModel.handleRawKeyEvent(event)
|
||||||
|
: null,
|
||||||
|
onKeyEvent: useRawKeyEvents
|
||||||
|
? null
|
||||||
|
: (FocusNode node, KeyEvent event) =>
|
||||||
|
inputModel.handleKeyEvent(event),
|
||||||
child: child));
|
child: child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +243,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||||
}
|
}
|
||||||
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
||||||
@@ -275,7 +285,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
ffi.cursorModel.clearRemoteWindowCoords();
|
ffi.cursorModel.clearRemoteWindowCoords();
|
||||||
}
|
}
|
||||||
inputModel.sendMouse('up', MouseButtons.left);
|
inputModel.sendMouse('up', MouseButtons.left);
|
||||||
|
|||||||
@@ -147,12 +147,23 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
child: Text(translate('Reset canvas')),
|
child: Text(translate('Reset canvas')),
|
||||||
onPressed: () => ffi.cursorModel.reset()));
|
onPressed: () => ffi.cursorModel.reset()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectWithToken(
|
||||||
|
{required bool isFileTransfer, required bool isTcpTunneling}) {
|
||||||
|
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||||
|
connect(context, id,
|
||||||
|
isFileTransfer: isFileTransfer,
|
||||||
|
isTcpTunneling: isTcpTunneling,
|
||||||
|
connToken: connToken);
|
||||||
|
}
|
||||||
|
|
||||||
// transferFile
|
// transferFile
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Text(translate('Transfer file')),
|
child: Text(translate('Transfer file')),
|
||||||
onPressed: () => connect(context, id, isFileTransfer: true)),
|
onPressed: () =>
|
||||||
|
connectWithToken(isFileTransfer: true, isTcpTunneling: false)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// tcpTunneling
|
// tcpTunneling
|
||||||
@@ -160,7 +171,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Text(translate('TCP tunneling')),
|
child: Text(translate('TCP tunneling')),
|
||||||
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
onPressed: () =>
|
||||||
|
connectWithToken(isFileTransfer: false, isTcpTunneling: true)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// note
|
// note
|
||||||
@@ -183,7 +195,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
||||||
v.add(
|
v.add(
|
||||||
TTextMenu(
|
TTextMenu(
|
||||||
child: Text('${translate("Insert")} Ctrl + Alt + Del'),
|
child: Text('${translate("Insert Ctrl + Alt + Del")}'),
|
||||||
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
|
|||||||
const String kPeerPlatformLinux = "Linux";
|
const String kPeerPlatformLinux = "Linux";
|
||||||
const String kPeerPlatformMacOS = "Mac OS";
|
const String kPeerPlatformMacOS = "Mac OS";
|
||||||
const String kPeerPlatformAndroid = "Android";
|
const String kPeerPlatformAndroid = "Android";
|
||||||
|
const String kPeerPlatformWebDesktop = "WebDesktop";
|
||||||
|
|
||||||
const double kScrollbarThickness = 12.0;
|
const double kScrollbarThickness = 12.0;
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
|
|||||||
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
||||||
const String kOptionEnableHwcodec = "enable-hwcodec";
|
const String kOptionEnableHwcodec = "enable-hwcodec";
|
||||||
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
||||||
|
const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing";
|
||||||
const String kOptionVideoSaveDirectory = "video-save-directory";
|
const String kOptionVideoSaveDirectory = "video-save-directory";
|
||||||
const String kOptionAccessMode = "access-mode";
|
const String kOptionAccessMode = "access-mode";
|
||||||
const String kOptionEnableKeyboard = "enable-keyboard";
|
const String kOptionEnableKeyboard = "enable-keyboard";
|
||||||
@@ -200,7 +202,7 @@ const double kMinFps = 5;
|
|||||||
const double kDefaultFps = 30;
|
const double kDefaultFps = 30;
|
||||||
const double kMaxFps = 120;
|
const double kMaxFps = 120;
|
||||||
|
|
||||||
const double kMinQuality = 10;
|
const double kMinQuality = 5;
|
||||||
const double kDefaultQuality = 50;
|
const double kDefaultQuality = 50;
|
||||||
const double kMaxQuality = 100;
|
const double kMaxQuality = 100;
|
||||||
const double kMaxMoreQuality = 2000;
|
const double kMaxMoreQuality = 2000;
|
||||||
@@ -569,3 +571,5 @@ enum WindowsTarget {
|
|||||||
extension WindowsTargetExt on int {
|
extension WindowsTargetExt on int {
|
||||||
WindowsTarget get windowsVersion => getWindowsTarget(this);
|
WindowsTarget get windowsVersion => getWindowsTarget(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:auto_size_text/auto_size_text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -323,36 +323,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
child: Ink(
|
child: Ink(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
getConnectionPageTitle(context, false).marginOnly(bottom: 15),
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
AutoSizeText(
|
|
||||||
translate('Control Remote Desktop'),
|
|
||||||
maxLines: 1,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleLarge
|
|
||||||
?.merge(TextStyle(height: 1)),
|
|
||||||
).marginOnly(right: 4),
|
|
||||||
Tooltip(
|
|
||||||
waitDuration: Duration(milliseconds: 300),
|
|
||||||
message: translate("id_input_tip"),
|
|
||||||
child: Icon(
|
|
||||||
Icons.help_outline_outlined,
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleLarge
|
|
||||||
?.color
|
|
||||||
?.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
).marginOnly(bottom: 15),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -664,9 +664,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (!bind.isCustomClient()) {
|
if (!bind.isCustomClient()) {
|
||||||
|
platformFFI.registerEventHandler(
|
||||||
|
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||||
|
(Map<String, dynamic> evt) async {
|
||||||
|
if (evt['url'] is String) {
|
||||||
|
setState(() {
|
||||||
|
updateUrl = evt['url'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Timer(const Duration(seconds: 1), () async {
|
Timer(const Duration(seconds: 1), () async {
|
||||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
bind.mainGetSoftwareUpdateUrl();
|
||||||
if (updateUrl.isNotEmpty) setState(() {});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||||
@@ -766,6 +774,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
isRDP: call.arguments['isRDP'],
|
isRDP: call.arguments['isRDP'],
|
||||||
password: call.arguments['password'],
|
password: call.arguments['password'],
|
||||||
forceRelay: call.arguments['forceRelay'],
|
forceRelay: call.arguments['forceRelay'],
|
||||||
|
connToken: call.arguments['connToken'],
|
||||||
);
|
);
|
||||||
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
||||||
final args = call.arguments.split(',');
|
final args = call.arguments.split(',');
|
||||||
@@ -824,6 +833,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
_uniLinksSubscription?.cancel();
|
_uniLinksSubscription?.cancel();
|
||||||
Get.delete<RxBool>(tag: 'stop-service');
|
Get.delete<RxBool>(tag: 'stop-service');
|
||||||
_updateTimer?.cancel();
|
_updateTimer?.cancel();
|
||||||
|
if (!bind.isCustomClient()) {
|
||||||
|
platformFFI.unregisterEventHandler(
|
||||||
|
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,6 +870,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
// SpecialCharacterValidationRule(),
|
// SpecialCharacterValidationRule(),
|
||||||
MinCharactersValidationRule(8),
|
MinCharactersValidationRule(8),
|
||||||
];
|
];
|
||||||
|
final maxLength = bind.mainMaxEncryptLen();
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
submit() {
|
submit() {
|
||||||
@@ -915,6 +929,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
errMsg0 = '';
|
errMsg0 = '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
maxLength: maxLength,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -941,6 +956,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
|||||||
errMsg1 = '';
|
errMsg1 = '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
maxLength: maxLength,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
final SettingsTabKey initialTabkey;
|
final SettingsTabKey initialTabkey;
|
||||||
static final List<SettingsTabKey> tabKeys = [
|
static final List<SettingsTabKey> tabKeys = [
|
||||||
SettingsTabKey.general,
|
SettingsTabKey.general,
|
||||||
if (!bind.isOutgoingOnly() &&
|
if (!isWeb &&
|
||||||
|
!bind.isOutgoingOnly() &&
|
||||||
!bind.isDisableSettings() &&
|
!bind.isDisableSettings() &&
|
||||||
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
||||||
SettingsTabKey.safety,
|
SettingsTabKey.safety,
|
||||||
@@ -216,7 +217,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
width: _kTabWidth,
|
width: _kTabWidth,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_header(),
|
_header(context),
|
||||||
Flexible(child: _listView(tabs: _settingTabs())),
|
Flexible(child: _listView(tabs: _settingTabs())),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -239,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(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
if (isWeb)
|
||||||
height: 62,
|
IconButton(
|
||||||
child: Text(
|
onPressed: () {
|
||||||
translate('Settings'),
|
if (Navigator.canPop(context)) {
|
||||||
textAlign: TextAlign.left,
|
Navigator.pop(context);
|
||||||
style: const TextStyle(
|
}
|
||||||
color: _accentColor,
|
},
|
||||||
fontSize: _kTitleFontSize,
|
icon: Icon(Icons.arrow_back),
|
||||||
fontWeight: FontWeight.w400,
|
).marginOnly(left: 5),
|
||||||
|
if (isWeb)
|
||||||
|
SizedBox(
|
||||||
|
height: 62,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: settingsText,
|
||||||
),
|
),
|
||||||
),
|
).marginOnly(left: 20),
|
||||||
).marginOnly(left: 20, top: 10),
|
if (!isWeb)
|
||||||
|
SizedBox(
|
||||||
|
height: 62,
|
||||||
|
child: settingsText,
|
||||||
|
).marginOnly(left: 20, top: 10),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -322,7 +342,8 @@ class _General extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GeneralState extends State<_General> {
|
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;
|
RxBool serviceBtnEnabled = true.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -334,13 +355,13 @@ class _GeneralState extends State<_General> {
|
|||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
physics: DraggableNeverScrollableScrollPhysics(),
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
children: [
|
children: [
|
||||||
service(),
|
if (!isWeb) service(),
|
||||||
theme(),
|
theme(),
|
||||||
_Card(title: 'Language', children: [language()]),
|
_Card(title: 'Language', children: [language()]),
|
||||||
hwcodec(),
|
if (!isWeb) hwcodec(),
|
||||||
audio(context),
|
if (!isWeb) audio(context),
|
||||||
record(context),
|
if (!isWeb) record(context),
|
||||||
WaylandCard(),
|
if (!isWeb) WaylandCard(),
|
||||||
other()
|
other()
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: _kListViewBottomMargin));
|
).marginOnly(bottom: _kListViewBottomMargin));
|
||||||
@@ -348,8 +369,8 @@ class _GeneralState extends State<_General> {
|
|||||||
|
|
||||||
Widget theme() {
|
Widget theme() {
|
||||||
final current = MyTheme.getThemeModePreference().toShortString();
|
final current = MyTheme.getThemeModePreference().toShortString();
|
||||||
onChanged(String value) {
|
onChanged(String value) async {
|
||||||
MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
|
await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,13 +415,13 @@ class _GeneralState extends State<_General> {
|
|||||||
|
|
||||||
Widget other() {
|
Widget other() {
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
if (!bind.isIncomingOnly())
|
if (!isWeb && !bind.isIncomingOnly())
|
||||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||||
kOptionEnableConfirmClosingTabs,
|
kOptionEnableConfirmClosingTabs,
|
||||||
isServer: false),
|
isServer: false),
|
||||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||||
wallpaper(),
|
if (!isWeb) wallpaper(),
|
||||||
if (!bind.isIncomingOnly()) ...[
|
if (!isWeb && !bind.isIncomingOnly()) ...[
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Open connection in new tab',
|
'Open connection in new tab',
|
||||||
@@ -417,18 +438,19 @@ class _GeneralState extends State<_General> {
|
|||||||
kOptionAllowAlwaysSoftwareRender,
|
kOptionAllowAlwaysSoftwareRender,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
if (!isWeb)
|
||||||
message: translate('texture_render_tip'),
|
Tooltip(
|
||||||
child: _OptionCheckBox(
|
message: translate('texture_render_tip'),
|
||||||
context,
|
child: _OptionCheckBox(
|
||||||
"Use texture rendering",
|
context,
|
||||||
kOptionTextureRender,
|
"Use texture rendering",
|
||||||
optGetter: bind.mainGetUseTextureRender,
|
kOptionTextureRender,
|
||||||
optSetter: (k, v) async =>
|
optGetter: bind.mainGetUseTextureRender,
|
||||||
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
|
optSetter: (k, v) async =>
|
||||||
|
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (!isWeb && !bind.isCustomClient())
|
||||||
if (!bind.isCustomClient())
|
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Check for software update on startup',
|
'Check for software update on startup',
|
||||||
@@ -443,7 +465,7 @@ class _GeneralState extends State<_General> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||||
children.add(_OptionCheckBox(
|
children.add(_OptionCheckBox(
|
||||||
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||||
}
|
}
|
||||||
@@ -553,12 +575,18 @@ class _GeneralState extends State<_General> {
|
|||||||
bool root_dir_exists = map['root_dir_exists']!;
|
bool root_dir_exists = map['root_dir_exists']!;
|
||||||
bool user_dir_exists = map['user_dir_exists']!;
|
bool user_dir_exists = map['user_dir_exists']!;
|
||||||
return _Card(title: 'Recording', children: [
|
return _Card(title: 'Recording', children: [
|
||||||
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
if (!bind.isOutgoingOnly())
|
||||||
kOptionAllowAutoRecordIncoming),
|
_OptionCheckBox(context, 'Automatically record incoming sessions',
|
||||||
if (showRootDir)
|
kOptionAllowAutoRecordIncoming),
|
||||||
|
if (!bind.isIncomingOnly())
|
||||||
|
_OptionCheckBox(context, 'Automatically record outgoing sessions',
|
||||||
|
kOptionAllowAutoRecordOutgoing,
|
||||||
|
isServer: false),
|
||||||
|
if (showRootDir && !bind.isOutgoingOnly())
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('${translate("Incoming")}:'),
|
Text(
|
||||||
|
'${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: root_dir_exists
|
onTap: root_dir_exists
|
||||||
@@ -575,45 +603,49 @@ class _GeneralState extends State<_General> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(left: _kContentHMargin),
|
).marginOnly(left: _kContentHMargin),
|
||||||
Row(
|
if (!(showRootDir && bind.isIncomingOnly()))
|
||||||
children: [
|
Row(
|
||||||
Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
|
children: [
|
||||||
Expanded(
|
Text(
|
||||||
child: GestureDetector(
|
'${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'),
|
||||||
onTap: user_dir_exists
|
Expanded(
|
||||||
? () => launchUrl(Uri.file(user_dir))
|
child: GestureDetector(
|
||||||
: null,
|
onTap: user_dir_exists
|
||||||
child: Text(
|
? () => launchUrl(Uri.file(user_dir))
|
||||||
user_dir,
|
|
||||||
softWrap: true,
|
|
||||||
style: user_dir_exists
|
|
||||||
? const TextStyle(decoration: TextDecoration.underline)
|
|
||||||
: null,
|
: null,
|
||||||
)).marginOnly(left: 10),
|
child: Text(
|
||||||
),
|
user_dir,
|
||||||
ElevatedButton(
|
softWrap: true,
|
||||||
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
|
style: user_dir_exists
|
||||||
? null
|
? const TextStyle(
|
||||||
: () async {
|
decoration: TextDecoration.underline)
|
||||||
String? initialDirectory;
|
: null,
|
||||||
if (await Directory.fromUri(Uri.directory(user_dir))
|
)).marginOnly(left: 10),
|
||||||
.exists()) {
|
),
|
||||||
initialDirectory = user_dir;
|
ElevatedButton(
|
||||||
}
|
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
|
||||||
String? selectedDirectory =
|
? null
|
||||||
await FilePicker.platform.getDirectoryPath(
|
: () async {
|
||||||
initialDirectory: initialDirectory);
|
String? initialDirectory;
|
||||||
if (selectedDirectory != null) {
|
if (await Directory.fromUri(
|
||||||
await bind.mainSetOption(
|
Uri.directory(user_dir))
|
||||||
key: kOptionVideoSaveDirectory,
|
.exists()) {
|
||||||
value: selectedDirectory);
|
initialDirectory = user_dir;
|
||||||
setState(() {});
|
}
|
||||||
}
|
String? selectedDirectory =
|
||||||
},
|
await FilePicker.platform.getDirectoryPath(
|
||||||
child: Text(translate('Change')))
|
initialDirectory: initialDirectory);
|
||||||
.marginOnly(left: 5),
|
if (selectedDirectory != null) {
|
||||||
],
|
await bind.mainSetLocalOption(
|
||||||
).marginOnly(left: _kContentHMargin),
|
key: kOptionVideoSaveDirectory,
|
||||||
|
value: selectedDirectory);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(translate('Change')))
|
||||||
|
.marginOnly(left: 5),
|
||||||
|
],
|
||||||
|
).marginOnly(left: _kContentHMargin),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -641,8 +673,9 @@ class _GeneralState extends State<_General> {
|
|||||||
initialKey: currentKey,
|
initialKey: currentKey,
|
||||||
onChanged: (key) async {
|
onChanged: (key) async {
|
||||||
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
|
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
|
||||||
reloadAllWindows();
|
if (isWeb) reloadCurrentWindow();
|
||||||
bind.mainChangeLanguage(lang: key);
|
if (!isWeb) reloadAllWindows();
|
||||||
|
if (!isWeb) bind.mainChangeLanguage(lang: key);
|
||||||
},
|
},
|
||||||
enabled: !isOptFixed,
|
enabled: !isOptFixed,
|
||||||
).marginOnly(left: _kContentHMargin);
|
).marginOnly(left: _kContentHMargin);
|
||||||
@@ -1337,7 +1370,7 @@ class _Network extends StatefulWidget {
|
|||||||
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
bool locked = bind.mainIsInstalled();
|
bool locked = !isWeb && bind.mainIsInstalled();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1346,8 +1379,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
|||||||
final scrollController = ScrollController();
|
final scrollController = ScrollController();
|
||||||
final hideServer =
|
final hideServer =
|
||||||
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||||
|
// TODO: support web proxy
|
||||||
final hideProxy =
|
final hideProxy =
|
||||||
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||||
return DesktopScrollWrapper(
|
return DesktopScrollWrapper(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@@ -1427,8 +1461,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
|||||||
children: [
|
children: [
|
||||||
Obx(() => _LabeledTextField(context, 'ID Server', idController,
|
Obx(() => _LabeledTextField(context, 'ID Server', idController,
|
||||||
idErrMsg.value, enabled, secure)),
|
idErrMsg.value, enabled, secure)),
|
||||||
Obx(() => _LabeledTextField(context, 'Relay Server',
|
if (!isWeb)
|
||||||
relayController, relayErrMsg.value, enabled, secure)),
|
Obx(() => _LabeledTextField(context, 'Relay Server',
|
||||||
|
relayController, relayErrMsg.value, enabled, secure)),
|
||||||
Obx(() => _LabeledTextField(context, 'API Server',
|
Obx(() => _LabeledTextField(context, 'API Server',
|
||||||
apiController, apiErrMsg.value, enabled, secure)),
|
apiController, apiErrMsg.value, enabled, secure)),
|
||||||
_LabeledTextField(
|
_LabeledTextField(
|
||||||
@@ -1467,7 +1502,7 @@ class _DisplayState extends State<_Display> {
|
|||||||
scrollStyle(context),
|
scrollStyle(context),
|
||||||
imageQuality(context),
|
imageQuality(context),
|
||||||
codec(context),
|
codec(context),
|
||||||
privacyModeImpl(context),
|
if (!isWeb) privacyModeImpl(context),
|
||||||
other(context),
|
other(context),
|
||||||
]).marginOnly(bottom: _kListViewBottomMargin));
|
]).marginOnly(bottom: _kListViewBottomMargin));
|
||||||
}
|
}
|
||||||
@@ -1878,9 +1913,10 @@ class _AboutState extends State<_About> {
|
|||||||
SelectionArea(
|
SelectionArea(
|
||||||
child: Text('${translate('Build Date')}: $buildDate')
|
child: Text('${translate('Build Date')}: $buildDate')
|
||||||
.marginSymmetric(vertical: 4.0)),
|
.marginSymmetric(vertical: 4.0)),
|
||||||
SelectionArea(
|
if (!isWeb)
|
||||||
child: Text('${translate('Fingerprint')}: $fingerprint')
|
SelectionArea(
|
||||||
.marginSymmetric(vertical: 4.0)),
|
child: Text('${translate('Fingerprint')}: $fingerprint')
|
||||||
|
.marginSymmetric(vertical: 4.0)),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString('https://rustdesk.com/privacy.html');
|
launchUrlString('https://rustdesk.com/privacy.html');
|
||||||
@@ -2487,6 +2523,7 @@ void changeSocks5Proxy() async {
|
|||||||
: Icons.visibility))),
|
: Icons.visibility))),
|
||||||
controller: pwdController,
|
controller: pwdController,
|
||||||
enabled: !isOptFixed,
|
enabled: !isOptFixed,
|
||||||
|
maxLength: bind.mainMaxEncryptLen(),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:extended_text/extended_text.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||||
import 'package:percent_indicator/percent_indicator.dart';
|
import 'package:percent_indicator/percent_indicator.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
@@ -16,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart';
|
|||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
import 'package:flutter_hbb/web/dummy.dart'
|
||||||
|
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
|
||||||
|
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||||
@@ -54,21 +57,23 @@ class FileManagerPage extends StatefulWidget {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.password,
|
required this.password,
|
||||||
required this.isSharedPassword,
|
required this.isSharedPassword,
|
||||||
required this.tabController,
|
this.tabController,
|
||||||
|
this.connToken,
|
||||||
this.forceRelay})
|
this.forceRelay})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
final String id;
|
final String id;
|
||||||
final String? password;
|
final String? password;
|
||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
final bool? forceRelay;
|
final bool? forceRelay;
|
||||||
final DesktopTabController tabController;
|
final String? connToken;
|
||||||
|
final DesktopTabController? tabController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FileManagerPageState extends State<FileManagerPage>
|
class _FileManagerPageState extends State<FileManagerPage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||||
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
||||||
|
|
||||||
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
||||||
@@ -87,6 +92,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
isFileTransfer: true,
|
isFileTransfer: true,
|
||||||
password: widget.password,
|
password: widget.password,
|
||||||
isSharedPassword: widget.isSharedPassword,
|
isSharedPassword: widget.isSharedPassword,
|
||||||
|
connToken: widget.connToken,
|
||||||
forceRelay: widget.forceRelay);
|
forceRelay: widget.forceRelay);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_ffi.dialogManager
|
_ffi.dialogManager
|
||||||
@@ -96,12 +102,16 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
if (!isLinux) {
|
if (!isLinux) {
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
}
|
}
|
||||||
|
if (isWeb) {
|
||||||
|
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||||
|
}
|
||||||
debugPrint("File manager page init success with id ${widget.id}");
|
debugPrint("File manager page init success with id ${widget.id}");
|
||||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
widget.tabController.onSelected?.call(widget.id);
|
widget.tabController?.onSelected?.call(widget.id);
|
||||||
});
|
});
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -114,12 +124,21 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
}
|
}
|
||||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||||
});
|
});
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
jobController.jobTable.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
@@ -129,10 +148,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
if (!isWeb)
|
||||||
flex: 3,
|
Flexible(
|
||||||
child: dropArea(FileManagerView(
|
flex: 3,
|
||||||
model.localController, _ffi, _mouseFocusScope))),
|
child: dropArea(FileManagerView(
|
||||||
|
model.localController, _ffi, _mouseFocusScope))),
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: dropArea(FileManagerView(
|
child: dropArea(FileManagerView(
|
||||||
@@ -173,10 +193,31 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
/// transfer status list
|
/// transfer status list
|
||||||
/// watch transfer status
|
/// watch transfer status
|
||||||
Widget statusList() {
|
Widget statusList() {
|
||||||
|
Widget getIcon(JobProgress job) {
|
||||||
|
final color = Theme.of(context).tabBarTheme.labelColor;
|
||||||
|
switch (job.type) {
|
||||||
|
case JobType.deleteDir:
|
||||||
|
case JobType.deleteFile:
|
||||||
|
return Icon(Icons.delete_outline, color: color);
|
||||||
|
default:
|
||||||
|
return Transform.rotate(
|
||||||
|
angle: isWeb
|
||||||
|
? job.isRemoteToLocal
|
||||||
|
? pi / 2
|
||||||
|
: pi / 2 * 3
|
||||||
|
: job.isRemoteToLocal
|
||||||
|
? pi
|
||||||
|
: 0,
|
||||||
|
child: Icon(Icons.arrow_forward_ios, color: color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
statusListView(List<JobProgress> jobs) => ListView.builder(
|
statusListView(List<JobProgress> jobs) => ListView.builder(
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final item = jobs[index];
|
final item = jobs[index];
|
||||||
|
final status = item.getStatus();
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 5),
|
padding: const EdgeInsets.only(bottom: 5),
|
||||||
child: generateCard(
|
child: generateCard(
|
||||||
@@ -186,15 +227,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Transform.rotate(
|
getIcon(item)
|
||||||
angle: item.isRemoteToLocal ? pi : 0,
|
.marginSymmetric(horizontal: 10, vertical: 12),
|
||||||
child: SvgPicture.asset("assets/arrow.svg",
|
|
||||||
colorFilter: svgColor(
|
|
||||||
Theme.of(context).tabBarTheme.labelColor)),
|
|
||||||
).paddingOnly(left: 15),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -203,45 +237,28 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Tooltip(
|
Tooltip(
|
||||||
waitDuration: Duration(milliseconds: 500),
|
waitDuration: Duration(milliseconds: 500),
|
||||||
message: item.jobName,
|
message: item.jobName,
|
||||||
child: Text(
|
child: ExtendedText(
|
||||||
item.fileName,
|
item.jobName,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
).paddingSymmetric(vertical: 10),
|
overflowWidget: TextOverflowWidget(
|
||||||
),
|
child: Text("..."),
|
||||||
Text(
|
position: TextOverflowPosition.start),
|
||||||
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: MyTheme.darkGray,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Offstage(
|
Tooltip(
|
||||||
offstage: item.state != JobState.inProgress,
|
waitDuration: Duration(milliseconds: 500),
|
||||||
child: Text(
|
message: status,
|
||||||
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
|
child: Text(status,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: MyTheme.darkGray,
|
color: MyTheme.darkGray,
|
||||||
),
|
)).marginOnly(top: 6),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: item.state == JobState.inProgress,
|
offstage: item.type != JobType.transfer ||
|
||||||
child: Text(
|
item.state != JobState.inProgress,
|
||||||
translate(
|
|
||||||
item.display(),
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: MyTheme.darkGray,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Offstage(
|
|
||||||
offstage: item.state != JobState.inProgress,
|
|
||||||
child: LinearPercentIndicator(
|
child: LinearPercentIndicator(
|
||||||
padding: EdgeInsets.only(right: 15),
|
|
||||||
animateFromLastPercent: true,
|
animateFromLastPercent: true,
|
||||||
center: Text(
|
center: Text(
|
||||||
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
||||||
@@ -251,7 +268,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
progressColor: MyTheme.accent,
|
progressColor: MyTheme.accent,
|
||||||
backgroundColor: Theme.of(context).hoverColor,
|
backgroundColor: Theme.of(context).hoverColor,
|
||||||
lineHeight: kDesktopFileTransferRowHeight,
|
lineHeight: kDesktopFileTransferRowHeight,
|
||||||
).paddingSymmetric(vertical: 15),
|
).paddingSymmetric(vertical: 8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -276,7 +293,6 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
),
|
),
|
||||||
MenuButton(
|
MenuButton(
|
||||||
tooltip: translate("Delete"),
|
tooltip: translate("Delete"),
|
||||||
padding: EdgeInsets.only(right: 15),
|
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
"assets/close.svg",
|
"assets/close.svg",
|
||||||
colorFilter: svgColor(Colors.white),
|
colorFilter: svgColor(Colors.white),
|
||||||
@@ -289,11 +305,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
hoverColor: MyTheme.accent80,
|
hoverColor: MyTheme.accent80,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
).marginAll(12),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(vertical: 10),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -477,6 +493,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget headTools() {
|
Widget headTools() {
|
||||||
|
var uploadButtonTapPosition = RelativeRect.fill;
|
||||||
|
RxBool isUploadFolder =
|
||||||
|
(bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
|
||||||
return Container(
|
return Container(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -799,6 +818,66 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isWeb)
|
||||||
|
Obx(() => ElevatedButton.icon(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||||
|
isLocal
|
||||||
|
? EdgeInsets.only(left: 10)
|
||||||
|
: EdgeInsets.only(right: 10)),
|
||||||
|
backgroundColor: MaterialStateProperty.all(
|
||||||
|
selectedItems.items.isEmpty
|
||||||
|
? MyTheme.accent80
|
||||||
|
: MyTheme.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
{webselectFiles(is_folder: isUploadFolder.value)},
|
||||||
|
label: InkWell(
|
||||||
|
hoverColor: Colors.transparent,
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
focusColor: Colors.transparent,
|
||||||
|
onTapDown: (e) {
|
||||||
|
final x = e.globalPosition.dx;
|
||||||
|
final y = e.globalPosition.dy;
|
||||||
|
uploadButtonTapPosition =
|
||||||
|
RelativeRect.fromLTRB(x, y, x, y);
|
||||||
|
},
|
||||||
|
onTap: () async {
|
||||||
|
final value = await showMenu<bool>(
|
||||||
|
context: context,
|
||||||
|
position: uploadButtonTapPosition,
|
||||||
|
items: [
|
||||||
|
PopupMenuItem<bool>(
|
||||||
|
value: false,
|
||||||
|
child: Text(translate('Upload files')),
|
||||||
|
),
|
||||||
|
PopupMenuItem<bool>(
|
||||||
|
value: true,
|
||||||
|
child: Text(translate('Upload folder')),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
if (value != null) {
|
||||||
|
isUploadFolder.value = value;
|
||||||
|
bind.mainSetLocalOption(
|
||||||
|
key: 'upload-folder-button',
|
||||||
|
value: value ? 'Y' : '');
|
||||||
|
webselectFiles(is_folder: value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Icon(Icons.arrow_drop_down),
|
||||||
|
),
|
||||||
|
icon: Text(
|
||||||
|
translate(isUploadFolder.isTrue
|
||||||
|
? 'Upload folder'
|
||||||
|
: 'Upload files'),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
).marginOnly(left: 8),
|
||||||
|
)).marginOnly(left: 16),
|
||||||
Obx(() => ElevatedButton.icon(
|
Obx(() => ElevatedButton.icon(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||||
@@ -832,19 +911,22 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
: Colors.white,
|
: Colors.white,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: RotatedBox(
|
: isWeb
|
||||||
quarterTurns: 2,
|
? Offstage()
|
||||||
child: SvgPicture.asset(
|
: RotatedBox(
|
||||||
"assets/arrow.svg",
|
quarterTurns: 2,
|
||||||
colorFilter: svgColor(selectedItems.items.isEmpty
|
child: SvgPicture.asset(
|
||||||
? Theme.of(context).brightness ==
|
"assets/arrow.svg",
|
||||||
Brightness.light
|
colorFilter: svgColor(
|
||||||
? MyTheme.grayBg
|
selectedItems.items.isEmpty
|
||||||
: MyTheme.darkGray
|
? Theme.of(context).brightness ==
|
||||||
: Colors.white),
|
Brightness.light
|
||||||
alignment: Alignment.bottomRight,
|
? MyTheme.grayBg
|
||||||
),
|
: MyTheme.darkGray
|
||||||
),
|
: Colors.white),
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
label: isLocal
|
label: isLocal
|
||||||
? SvgPicture.asset(
|
? SvgPicture.asset(
|
||||||
"assets/arrow.svg",
|
"assets/arrow.svg",
|
||||||
@@ -856,7 +938,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
: Colors.white),
|
: Colors.white),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
translate('Receive'),
|
translate(isWeb ? 'Download' : 'Receive'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: selectedItems.items.isEmpty
|
color: selectedItems.items.isEmpty
|
||||||
? Theme.of(context).brightness ==
|
? Theme.of(context).brightness ==
|
||||||
@@ -943,6 +1025,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
BuildContext context, ScrollController scrollController) {
|
BuildContext context, ScrollController scrollController) {
|
||||||
final fd = controller.directory.value;
|
final fd = controller.directory.value;
|
||||||
final entries = fd.entries;
|
final entries = fd.entries;
|
||||||
|
Rx<Entry?> rightClickEntry = Rx(null);
|
||||||
|
|
||||||
return ListSearchActionListener(
|
return ListSearchActionListener(
|
||||||
node: _keyboardNode,
|
node: _keyboardNode,
|
||||||
@@ -1002,16 +1085,69 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
? " "
|
? " "
|
||||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||||
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
|
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||||
|
onTap() {
|
||||||
|
final items = selectedItems;
|
||||||
|
// handle double click
|
||||||
|
if (_checkDoubleClick(entry)) {
|
||||||
|
controller.openDirectory(entry.path);
|
||||||
|
items.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_onSelectedChanged(items, filteredEntries, entry, isLocal);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSecondaryTap() {
|
||||||
|
final items = [
|
||||||
|
if (!entry.isDrive &&
|
||||||
|
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
|
||||||
|
mod_menu.PopupMenuItem(
|
||||||
|
child: Text(translate("Rename")),
|
||||||
|
height: CustomPopupMenuTheme.height,
|
||||||
|
onTap: () {
|
||||||
|
controller.renameAction(entry, isLocal);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
rightClickEntry.value = entry;
|
||||||
|
final future = mod_menu.showMenu(
|
||||||
|
context: context,
|
||||||
|
position: secondaryPosition,
|
||||||
|
items: items,
|
||||||
|
);
|
||||||
|
future.then((value) {
|
||||||
|
rightClickEntry.value = null;
|
||||||
|
});
|
||||||
|
future.onError((error, stackTrace) {
|
||||||
|
rightClickEntry.value = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSecondaryTapDown(details) {
|
||||||
|
secondaryPosition = RelativeRect.fromLTRB(
|
||||||
|
details.globalPosition.dx,
|
||||||
|
details.globalPosition.dy,
|
||||||
|
details.globalPosition.dx,
|
||||||
|
details.globalPosition.dy);
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 1),
|
padding: EdgeInsets.symmetric(vertical: 1),
|
||||||
child: Obx(() => Container(
|
child: Obx(() => Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: selectedItems.items.contains(entry)
|
color: selectedItems.items.contains(entry)
|
||||||
? Theme.of(context).hoverColor
|
? MyTheme.button
|
||||||
: Theme.of(context).cardColor,
|
: Theme.of(context).cardColor,
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.all(
|
||||||
Radius.circular(5.0),
|
Radius.circular(5.0),
|
||||||
),
|
),
|
||||||
|
border: rightClickEntry.value == entry
|
||||||
|
? Border.all(
|
||||||
|
color: MyTheme.button,
|
||||||
|
width: 1.0,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
key: ValueKey(entry.name),
|
key: ValueKey(entry.name),
|
||||||
height: kDesktopFileTransferRowHeight,
|
height: kDesktopFileTransferRowHeight,
|
||||||
@@ -1050,51 +1186,19 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(entry.name.nonBreaking,
|
child: Text(entry.name.nonBreaking,
|
||||||
|
style: TextStyle(
|
||||||
|
color: selectedItems.items
|
||||||
|
.contains(entry)
|
||||||
|
? Colors.white
|
||||||
|
: null),
|
||||||
overflow:
|
overflow:
|
||||||
TextOverflow.ellipsis))
|
TextOverflow.ellipsis))
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: onTap,
|
||||||
final items = selectedItems;
|
onSecondaryTap: onSecondaryTap,
|
||||||
// handle double click
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
if (_checkDoubleClick(entry)) {
|
|
||||||
controller.openDirectory(entry.path);
|
|
||||||
items.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_onSelectedChanged(
|
|
||||||
items, filteredEntries, entry, isLocal);
|
|
||||||
},
|
|
||||||
onSecondaryTap: () {
|
|
||||||
final items = [
|
|
||||||
if (!entry.isDrive &&
|
|
||||||
versionCmp(_ffi.ffiModel.pi.version,
|
|
||||||
"1.3.0") >=
|
|
||||||
0)
|
|
||||||
mod_menu.PopupMenuItem(
|
|
||||||
child: Text("Rename"),
|
|
||||||
height: CustomPopupMenuTheme.height,
|
|
||||||
onTap: () {
|
|
||||||
controller.renameAction(entry, isLocal);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
];
|
|
||||||
if (items.isNotEmpty) {
|
|
||||||
mod_menu.showMenu(
|
|
||||||
context: context,
|
|
||||||
position: secondaryPosition,
|
|
||||||
items: items,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSecondaryTapDown: (details) {
|
|
||||||
secondaryPosition = RelativeRect.fromLTRB(
|
|
||||||
details.globalPosition.dx,
|
|
||||||
details.globalPosition.dy,
|
|
||||||
details.globalPosition.dx,
|
|
||||||
details.globalPosition.dy);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
@@ -1111,11 +1215,17 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: MyTheme.darkGray,
|
color: selectedItems.items
|
||||||
|
.contains(entry)
|
||||||
|
? Colors.white70
|
||||||
|
: MyTheme.darkGray,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
onSecondaryTap: onSecondaryTap,
|
||||||
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
),
|
),
|
||||||
// Divider from header.
|
// Divider from header.
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -1131,9 +1241,16 @@ class _FileManagerViewState extends State<FileManagerView> {
|
|||||||
sizeStr,
|
sizeStr,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10, color: MyTheme.darkGray),
|
fontSize: 10,
|
||||||
|
color:
|
||||||
|
selectedItems.items.contains(entry)
|
||||||
|
? Colors.white70
|
||||||
|
: MyTheme.darkGray),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
onSecondaryTap: onSecondaryTap,
|
||||||
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
isSharedPassword: params['isSharedPassword'],
|
isSharedPassword: params['isSharedPassword'],
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
forceRelay: params['forceRelay'],
|
forceRelay: params['forceRelay'],
|
||||||
|
connToken: params['connToken'],
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
print(
|
debugPrint(
|
||||||
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
||||||
// for simplify, just replace connectionId
|
// for simplify, just replace connectionId
|
||||||
if (call.method == kWindowEventNewFileTransfer) {
|
if (call.method == kWindowEventNewFileTransfer) {
|
||||||
@@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
isSharedPassword: args['isSharedPassword'],
|
isSharedPassword: args['isSharedPassword'],
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
forceRelay: args['forceRelay'],
|
forceRelay: args['forceRelay'],
|
||||||
|
connToken: args['connToken'],
|
||||||
)));
|
)));
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget {
|
|||||||
required this.isRDP,
|
required this.isRDP,
|
||||||
required this.isSharedPassword,
|
required this.isSharedPassword,
|
||||||
this.forceRelay,
|
this.forceRelay,
|
||||||
|
this.connToken,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
final String id;
|
final String id;
|
||||||
final String? password;
|
final String? password;
|
||||||
@@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget {
|
|||||||
final bool isRDP;
|
final bool isRDP;
|
||||||
final bool? forceRelay;
|
final bool? forceRelay;
|
||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
|
final String? connToken;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PortForwardPage> createState() => _PortForwardPageState();
|
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||||
@@ -62,6 +64,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
password: widget.password,
|
password: widget.password,
|
||||||
isSharedPassword: widget.isSharedPassword,
|
isSharedPassword: widget.isSharedPassword,
|
||||||
forceRelay: widget.forceRelay,
|
forceRelay: widget.forceRelay,
|
||||||
|
connToken: widget.connToken,
|
||||||
isRdp: widget.isRDP);
|
isRdp: widget.isRDP);
|
||||||
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||||
debugPrint("Port forward page init success with id ${widget.id}");
|
debugPrint("Port forward page init success with id ${widget.id}");
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
isRDP: isRDP,
|
isRDP: isRDP,
|
||||||
forceRelay: params['forceRelay'],
|
forceRelay: params['forceRelay'],
|
||||||
|
connToken: params['connToken'],
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
isRDP: isRDP,
|
isRDP: isRDP,
|
||||||
tabController: tabController,
|
tabController: tabController,
|
||||||
forceRelay: args['forceRelay'],
|
forceRelay: args['forceRelay'],
|
||||||
|
connToken: args['connToken'],
|
||||||
)));
|
)));
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
|
_ffi.recordingModel
|
||||||
|
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
||||||
});
|
});
|
||||||
_ffi.start(
|
_ffi.start(
|
||||||
widget.id,
|
widget.id,
|
||||||
@@ -245,13 +247,14 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||||
// ensure we leave this session, this is a double check
|
if (closeSession) {
|
||||||
_ffi.inputModel.enterOrLeave(false);
|
// ensure we leave this session, this is a double check
|
||||||
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
|
}
|
||||||
DesktopMultiWindow.removeListener(this);
|
DesktopMultiWindow.removeListener(this);
|
||||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||||
_ffi.imageModel.disposeImage();
|
_ffi.imageModel.disposeImage();
|
||||||
_ffi.cursorModel.disposeImages();
|
_ffi.cursorModel.disposeImages();
|
||||||
_ffi.recordingModel.onClose();
|
|
||||||
_rawKeyFocusNode.dispose();
|
_rawKeyFocusNode.dispose();
|
||||||
await _ffi.close(closeSession: closeSession);
|
await _ffi.close(closeSession: closeSession);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
RemoteCountState.find().value = tabController.length;
|
RemoteCountState.find().value = tabController.length;
|
||||||
|
|
||||||
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
||||||
print(
|
debugPrint(
|
||||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||||
|
|
||||||
dynamic returnValue;
|
dynamic returnValue;
|
||||||
|
|||||||
@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
|
|||||||
localPlatform = kPeerPlatformWindows;
|
localPlatform = kPeerPlatformWindows;
|
||||||
} else if (isLinux) {
|
} else if (isLinux) {
|
||||||
localPlatform = kPeerPlatformLinux;
|
localPlatform = kPeerPlatformLinux;
|
||||||
|
} else if (isWebOnWindows || isWebOnLinux) {
|
||||||
|
localPlatform = kPeerPlatformWebDesktop;
|
||||||
}
|
}
|
||||||
// to-do: web desktop support ?
|
|
||||||
return localPlatform;
|
return localPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ class RemoteMenuEntry {
|
|||||||
}) {
|
}) {
|
||||||
return MenuEntryButton<String>(
|
return MenuEntryButton<String>(
|
||||||
childBuilder: (TextStyle? style) => Text(
|
childBuilder: (TextStyle? style) => Text(
|
||||||
'${translate("Insert")} Ctrl + Alt + Del',
|
translate("Insert Ctrl + Alt + Del"),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
proc: () {
|
proc: () {
|
||||||
@@ -436,6 +436,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
shadowColor: MyTheme.color(context).shadow,
|
shadowColor: MyTheme.color(context).shadow,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: _DraggableShowHide(
|
child: _DraggableShowHide(
|
||||||
|
id: widget.id,
|
||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
dragging: _dragging,
|
dragging: _dragging,
|
||||||
fractionX: _fractionX,
|
fractionX: _fractionX,
|
||||||
@@ -452,8 +453,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
|
|
||||||
Widget _buildToolbar(BuildContext context) {
|
Widget _buildToolbar(BuildContext context) {
|
||||||
final List<Widget> toolbarItems = [];
|
final List<Widget> toolbarItems = [];
|
||||||
|
toolbarItems.add(_PinMenu(state: widget.state));
|
||||||
if (!isWebDesktop) {
|
if (!isWebDesktop) {
|
||||||
toolbarItems.add(_PinMenu(state: widget.state));
|
|
||||||
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,8 +479,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
setFullscreen: _setFullscreen,
|
setFullscreen: _setFullscreen,
|
||||||
));
|
));
|
||||||
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
|
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
|
||||||
|
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
||||||
if (!isWeb) {
|
if (!isWeb) {
|
||||||
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
|
|
||||||
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
|
||||||
}
|
}
|
||||||
if (!isWeb) toolbarItems.add(_RecordMenu());
|
if (!isWeb) toolbarItems.add(_RecordMenu());
|
||||||
@@ -1612,7 +1613,9 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
// If use flutter to grab keys, we can only use one mode.
|
// If use flutter to grab keys, we can only use one mode.
|
||||||
// Map mode and Legacy mode, at least one of them is supported.
|
// Map mode and Legacy mode, at least one of them is supported.
|
||||||
String? modeOnly;
|
String? modeOnly;
|
||||||
if (isInputSourceFlutter) {
|
// Keep both map and legacy mode on web at the moment.
|
||||||
|
// TODO: Remove legacy mode after web supports translate mode on web.
|
||||||
|
if (isInputSourceFlutter && isDesktop) {
|
||||||
if (bind.sessionIsKeyboardModeSupported(
|
if (bind.sessionIsKeyboardModeSupported(
|
||||||
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
||||||
modeOnly = kKeyMapMode;
|
modeOnly = kKeyMapMode;
|
||||||
@@ -1716,7 +1719,9 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionToggleOption(
|
await bind.sessionToggleOption(
|
||||||
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||||
ffiModel.setViewOnly(id, value);
|
final viewOnly = await bind.sessionGetToggleOption(
|
||||||
|
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
|
||||||
|
ffiModel.setViewOnly(id, viewOnly ?? value);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
@@ -1776,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _IconSubmenuButton(
|
if (isWeb) {
|
||||||
tooltip: 'Chat',
|
return buildTextChatButton();
|
||||||
key: chatButtonKey,
|
} else {
|
||||||
svg: 'assets/chat.svg',
|
return _IconSubmenuButton(
|
||||||
ffi: widget.ffi,
|
tooltip: 'Chat',
|
||||||
color: _ToolbarTheme.blueColor,
|
key: chatButtonKey,
|
||||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
svg: 'assets/chat.svg',
|
||||||
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
ffi: widget.ffi,
|
||||||
|
color: _ToolbarTheme.blueColor,
|
||||||
|
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||||
|
menuChildrenGetter: () => [textChat(), voiceCall()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTextChatButton() {
|
||||||
|
return _IconMenuButton(
|
||||||
|
assetName: 'assets/message_24dp_5F6368.svg',
|
||||||
|
tooltip: 'Text chat',
|
||||||
|
key: chatButtonKey,
|
||||||
|
onPressed: _textChatOnPressed,
|
||||||
|
color: _ToolbarTheme.blueColor,
|
||||||
|
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
textChat() {
|
textChat() {
|
||||||
return MenuButton(
|
return MenuButton(
|
||||||
child: Text(translate('Text chat')),
|
child: Text(translate('Text chat')),
|
||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
onPressed: () {
|
onPressed: _textChatOnPressed);
|
||||||
RenderBox? renderBox =
|
}
|
||||||
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
|
|
||||||
Offset? initPos;
|
_textChatOnPressed() {
|
||||||
if (renderBox != null) {
|
RenderBox? renderBox =
|
||||||
final pos = renderBox.localToGlobal(Offset.zero);
|
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
Offset? initPos;
|
||||||
}
|
if (renderBox != null) {
|
||||||
|
final pos = renderBox.localToGlobal(Offset.zero);
|
||||||
widget.ffi.chatModel.changeCurrentKey(
|
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
|
||||||
MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
}
|
||||||
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
widget.ffi.chatModel
|
||||||
});
|
.changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
|
||||||
|
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
voiceCall() {
|
voiceCall() {
|
||||||
@@ -1904,8 +1924,7 @@ class _RecordMenu extends StatelessWidget {
|
|||||||
var ffi = Provider.of<FfiModel>(context);
|
var ffi = Provider.of<FfiModel>(context);
|
||||||
var recordingModel = Provider.of<RecordingModel>(context);
|
var recordingModel = Provider.of<RecordingModel>(context);
|
||||||
final visible =
|
final visible =
|
||||||
(recordingModel.start || ffi.permissions['recording'] != false) &&
|
(recordingModel.start || ffi.permissions['recording'] != false);
|
||||||
ffi.pi.currentDisplay != kAllDisplayValue;
|
|
||||||
if (!visible) return Offstage();
|
if (!visible) return Offstage();
|
||||||
return _IconMenuButton(
|
return _IconMenuButton(
|
||||||
assetName: 'assets/rec.svg',
|
assetName: 'assets/rec.svg',
|
||||||
@@ -2214,6 +2233,7 @@ class RdoMenuButton<T> extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableShowHide extends StatefulWidget {
|
class _DraggableShowHide extends StatefulWidget {
|
||||||
|
final String id;
|
||||||
final SessionID sessionId;
|
final SessionID sessionId;
|
||||||
final RxDouble fractionX;
|
final RxDouble fractionX;
|
||||||
final RxBool dragging;
|
final RxBool dragging;
|
||||||
@@ -2225,6 +2245,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
|
|
||||||
const _DraggableShowHide({
|
const _DraggableShowHide({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
required this.id,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.fractionX,
|
required this.fractionX,
|
||||||
required this.dragging,
|
required this.dragging,
|
||||||
@@ -2314,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
);
|
);
|
||||||
final isFullscreen = stateGlobal.fullscreen;
|
final isFullscreen = stateGlobal.fullscreen;
|
||||||
const double iconSize = 20;
|
const double iconSize = 20;
|
||||||
|
|
||||||
|
buttonWrapper(VoidCallback? onPressed, Widget child,
|
||||||
|
{Color hoverColor = _ToolbarTheme.blueColor}) {
|
||||||
|
final bgColor = buttonStyle.backgroundColor?.resolve({});
|
||||||
|
return TextButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: child,
|
||||||
|
style: buttonStyle.copyWith(
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||||
|
if (states.contains(MaterialState.hovered)) {
|
||||||
|
return (bgColor ?? hoverColor).withOpacity(0.15);
|
||||||
|
}
|
||||||
|
return bgColor;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final child = Row(
|
final child = Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildDraggable(context),
|
_buildDraggable(context),
|
||||||
Obx(() => TextButton(
|
Obx(() => buttonWrapper(
|
||||||
onPressed: () {
|
() {
|
||||||
widget.setFullscreen(!isFullscreen.value);
|
widget.setFullscreen(!isFullscreen.value);
|
||||||
},
|
},
|
||||||
child: Tooltip(
|
Tooltip(
|
||||||
message: translate(
|
message: translate(
|
||||||
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
|
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -2333,12 +2372,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
if (!isMacOS)
|
if (!isMacOS && !isWebDesktop)
|
||||||
Obx(() => Offstage(
|
Obx(() => Offstage(
|
||||||
offstage: isFullscreen.isFalse,
|
offstage: isFullscreen.isFalse,
|
||||||
child: TextButton(
|
child: buttonWrapper(
|
||||||
onPressed: () => widget.setMinimize(),
|
widget.setMinimize,
|
||||||
child: Tooltip(
|
Tooltip(
|
||||||
message: translate('Minimize'),
|
message: translate('Minimize'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.remove,
|
Icons.remove,
|
||||||
@@ -2347,11 +2386,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
TextButton(
|
buttonWrapper(
|
||||||
onPressed: () => setState(() {
|
() => setState(() {
|
||||||
widget.toolbarState.switchShow(widget.sessionId);
|
widget.toolbarState.switchShow(widget.sessionId);
|
||||||
}),
|
}),
|
||||||
child: Obx((() => Tooltip(
|
Obx((() => Tooltip(
|
||||||
message:
|
message:
|
||||||
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -2360,6 +2399,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
))),
|
))),
|
||||||
),
|
),
|
||||||
|
if (isWebDesktop)
|
||||||
|
Obx(() {
|
||||||
|
if (show.isTrue) {
|
||||||
|
return Offstage();
|
||||||
|
} else {
|
||||||
|
return buttonWrapper(
|
||||||
|
() => closeConnection(id: widget.id),
|
||||||
|
Tooltip(
|
||||||
|
message: translate('Close'),
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: iconSize,
|
||||||
|
color: _ToolbarTheme.redColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hoverColor: _ToolbarTheme.redColor,
|
||||||
|
).paddingOnly(left: iconSize / 2);
|
||||||
|
}
|
||||||
|
})
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return TextButtonTheme(
|
return TextButtonTheme(
|
||||||
|
|||||||
@@ -552,6 +552,13 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
controller: state.value.pageController,
|
controller: state.value.pageController,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
children: () {
|
children: () {
|
||||||
|
if (DesktopTabType.cm == tabType) {
|
||||||
|
// Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful.
|
||||||
|
return state.value.tabs.map((tab) {
|
||||||
|
return tab.page;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
/// to-do refactor, separate connection state and UI state for remote session.
|
/// to-do refactor, separate connection state and UI state for remote session.
|
||||||
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
||||||
final tabLen = state.value.tabs.length;
|
final tabLen = state.value.tabs.length;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ WindowType? kWindowType;
|
|||||||
late List<String> kBootArgs;
|
late List<String> kBootArgs;
|
||||||
|
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
|
earlyAssert();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
debugPrint("launch args: $args");
|
debugPrint("launch args: $args");
|
||||||
@@ -161,7 +162,7 @@ void runMobileApp() async {
|
|||||||
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
|
||||||
gFFI.userModel.refreshCurrentUser();
|
gFFI.userModel.refreshCurrentUser();
|
||||||
runApp(App());
|
runApp(App());
|
||||||
if (!isWeb) await initUniLinks();
|
await initUniLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
void runMultiWindow(
|
void runMultiWindow(
|
||||||
@@ -372,7 +373,7 @@ class App extends StatefulWidget {
|
|||||||
State<App> createState() => _AppState();
|
State<App> createState() => _AppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppState extends State<App> {
|
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -396,6 +397,34 @@ class _AppState extends State<App> {
|
|||||||
bind.mainChangeTheme(dark: to.toShortString());
|
bind.mainChangeTheme(dark: to.toShortString());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeMetrics() {
|
||||||
|
_updateOrientation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateOrientation() {
|
||||||
|
if (isDesktop) return;
|
||||||
|
|
||||||
|
// Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
|
||||||
|
// my test (Flutter 3.19.6, Android 14) is always the reverse value.
|
||||||
|
// https://github.com/flutter/flutter/issues/60899
|
||||||
|
// stateGlobal.isPortrait.value =
|
||||||
|
// MediaQuery.of(context).orientation == Orientation.portrait;
|
||||||
|
|
||||||
|
final orientation = View.of(context).physicalSize.aspectRatio > 1
|
||||||
|
? Orientation.landscape
|
||||||
|
: Orientation.portrait;
|
||||||
|
stateGlobal.isPortrait.value = orientation == Orientation.portrait;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -416,7 +445,9 @@ class _AppState extends State<App> {
|
|||||||
child: GetMaterialApp(
|
child: GetMaterialApp(
|
||||||
navigatorKey: globalKey,
|
navigatorKey: globalKey,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'RustDesk',
|
title: isWeb
|
||||||
|
? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)'
|
||||||
|
: bind.mainGetAppNameSync(),
|
||||||
theme: MyTheme.lightTheme,
|
theme: MyTheme.lightTheme,
|
||||||
darkTheme: MyTheme.darkTheme,
|
darkTheme: MyTheme.darkTheme,
|
||||||
themeMode: MyTheme.currentThemeMode(),
|
themeMode: MyTheme.currentThemeMode(),
|
||||||
@@ -447,7 +478,8 @@ class _AppState extends State<App> {
|
|||||||
: (context, child) {
|
: (context, child) {
|
||||||
child = _keepScaleBuilder(context, child);
|
child = _keepScaleBuilder(context, child);
|
||||||
child = botToastBuilder(context, child);
|
child = botToastBuilder(context, child);
|
||||||
if (isDesktop && desktopType == DesktopType.main) {
|
if ((isDesktop && desktopType == DesktopType.main) ||
|
||||||
|
isWebDesktop) {
|
||||||
child = keyListenerBuilder(context, child);
|
child = keyListenerBuilder(context, child);
|
||||||
}
|
}
|
||||||
if (isLinux) {
|
if (isLinux) {
|
||||||
@@ -475,7 +507,7 @@ _registerEventHandler() {
|
|||||||
platformFFI.registerEventHandler('theme', 'theme', (evt) async {
|
platformFFI.registerEventHandler('theme', 'theme', (evt) async {
|
||||||
String? dark = evt['dark'];
|
String? dark = evt['dark'];
|
||||||
if (dark != null) {
|
if (dark != null) {
|
||||||
MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
|
await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
platformFFI.registerEventHandler('language', 'language', (_) async {
|
platformFFI.registerEventHandler('language', 'language', (_) async {
|
||||||
|
|||||||
@@ -3,25 +3,23 @@ import 'dart:async';
|
|||||||
import 'package:auto_size_text_field/auto_size_text_field.dart';
|
import 'package:auto_size_text_field/auto_size_text_field.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/widgets/login.dart';
|
|
||||||
import '../../common/widgets/peer_tab_page.dart';
|
import '../../common/widgets/peer_tab_page.dart';
|
||||||
import '../../common/widgets/autocomplete.dart';
|
import '../../common/widgets/autocomplete.dart';
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
import 'scan_page.dart';
|
|
||||||
import 'settings_page.dart';
|
|
||||||
|
|
||||||
/// Connection page for connecting to a remote peer.
|
/// Connection page for connecting to a remote peer.
|
||||||
class ConnectionPage extends StatefulWidget implements PageShape {
|
class ConnectionPage extends StatefulWidget implements PageShape {
|
||||||
ConnectionPage({Key? key}) : super(key: key);
|
ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final icon = const Icon(Icons.connected_tv);
|
final icon = const Icon(Icons.connected_tv);
|
||||||
@@ -30,7 +28,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
|
|||||||
final title = translate("Connection");
|
final title = translate("Connection");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
|
final List<Widget> appBarActions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ConnectionPage> createState() => _ConnectionPageState();
|
State<ConnectionPage> createState() => _ConnectionPageState();
|
||||||
@@ -73,9 +71,17 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
}
|
}
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
if (!bind.isCustomClient()) {
|
if (!bind.isCustomClient()) {
|
||||||
|
platformFFI.registerEventHandler(
|
||||||
|
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||||
|
(Map<String, dynamic> evt) async {
|
||||||
|
if (evt['url'] is String) {
|
||||||
|
setState(() {
|
||||||
|
_updateUrl = evt['url'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Timer(const Duration(seconds: 1), () async {
|
Timer(const Duration(seconds: 1), () async {
|
||||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
bind.mainGetSoftwareUpdateUrl();
|
||||||
if (_updateUrl.isNotEmpty) setState(() {});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,6 +212,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
FocusNode fieldFocusNode,
|
FocusNode fieldFocusNode,
|
||||||
VoidCallback onFieldSubmitted) {
|
VoidCallback onFieldSubmitted) {
|
||||||
fieldTextEditingController.text = _idController.text;
|
fieldTextEditingController.text = _idController.text;
|
||||||
|
Get.put<TextEditingController>(
|
||||||
|
fieldTextEditingController);
|
||||||
fieldFocusNode.addListener(() async {
|
fieldFocusNode.addListener(() async {
|
||||||
_idEmpty.value =
|
_idEmpty.value =
|
||||||
fieldTextEditingController.text.isEmpty;
|
fieldTextEditingController.text.isEmpty;
|
||||||
@@ -252,6 +260,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
inputFormatters: [IDTextInputFormatter()],
|
inputFormatters: [IDTextInputFormatter()],
|
||||||
|
onSubmitted: (_) {
|
||||||
|
onConnect();
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSelected: (option) {
|
onSelected: (option) {
|
||||||
@@ -341,9 +352,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final child = Column(children: [
|
||||||
|
if (isWebDesktop)
|
||||||
|
getConnectionPageTitle(context, true)
|
||||||
|
.marginOnly(bottom: 10, top: 15, left: 12),
|
||||||
|
w
|
||||||
|
]);
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Container(constraints: kMobilePageConstraints, child: w));
|
child: Container(constraints: kMobilePageConstraints, child: child));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -353,76 +370,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
if (Get.isRegistered<IDTextEditingController>()) {
|
if (Get.isRegistered<IDTextEditingController>()) {
|
||||||
Get.delete<IDTextEditingController>();
|
Get.delete<IDTextEditingController>();
|
||||||
}
|
}
|
||||||
|
if (Get.isRegistered<TextEditingController>()) {
|
||||||
|
Get.delete<TextEditingController>();
|
||||||
|
}
|
||||||
|
if (!bind.isCustomClient()) {
|
||||||
|
platformFFI.unregisterEventHandler(
|
||||||
|
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebMenu extends StatefulWidget {
|
|
||||||
const WebMenu({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<WebMenu> createState() => _WebMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WebMenuState extends State<WebMenu> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Provider.of<FfiModel>(context);
|
|
||||||
return PopupMenuButton<String>(
|
|
||||||
tooltip: "",
|
|
||||||
icon: const Icon(Icons.more_vert),
|
|
||||||
itemBuilder: (context) {
|
|
||||||
return (isIOS
|
|
||||||
? [
|
|
||||||
const PopupMenuItem(
|
|
||||||
value: "scan",
|
|
||||||
child: Icon(Icons.qr_code_scanner, color: Colors.black),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
: <PopupMenuItem<String>>[]) +
|
|
||||||
[
|
|
||||||
PopupMenuItem(
|
|
||||||
value: "server",
|
|
||||||
child: Text(translate('ID/Relay Server')),
|
|
||||||
)
|
|
||||||
] +
|
|
||||||
[
|
|
||||||
PopupMenuItem(
|
|
||||||
value: "login",
|
|
||||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
|
||||||
? translate("Login")
|
|
||||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
|
||||||
)
|
|
||||||
] +
|
|
||||||
[
|
|
||||||
PopupMenuItem(
|
|
||||||
value: "about",
|
|
||||||
child: Text(translate('About RustDesk')),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
},
|
|
||||||
onSelected: (value) {
|
|
||||||
if (value == 'server') {
|
|
||||||
showServerSettings(gFFI.dialogManager);
|
|
||||||
}
|
|
||||||
if (value == 'about') {
|
|
||||||
showAbout(gFFI.dialogManager);
|
|
||||||
}
|
|
||||||
if (value == 'login') {
|
|
||||||
if (gFFI.userModel.userName.value.isEmpty) {
|
|
||||||
loginDialog();
|
|
||||||
} else {
|
|
||||||
logOutConfirmDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (value == 'scan') {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (BuildContext context) => ScanPage(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/mobile/pages/server_page.dart';
|
import 'package:flutter_hbb/mobile/pages/server_page.dart';
|
||||||
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||||
|
import 'package:flutter_hbb/web/settings_page.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/widgets/chat_page.dart';
|
import '../../common/widgets/chat_page.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
import 'connection_page.dart';
|
import 'connection_page.dart';
|
||||||
|
|
||||||
abstract class PageShape extends Widget {
|
abstract class PageShape extends Widget {
|
||||||
@@ -45,7 +47,11 @@ class HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
void initPages() {
|
void initPages() {
|
||||||
_pages.clear();
|
_pages.clear();
|
||||||
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
|
if (!bind.isIncomingOnly()) {
|
||||||
|
_pages.add(ConnectionPage(
|
||||||
|
appBarActions: [],
|
||||||
|
));
|
||||||
|
}
|
||||||
if (isAndroid && !bind.isOutgoingOnly()) {
|
if (isAndroid && !bind.isOutgoingOnly()) {
|
||||||
_chatPageTabIndex = _pages.length;
|
_chatPageTabIndex = _pages.length;
|
||||||
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
||||||
@@ -149,18 +155,80 @@ class HomePageState extends State<HomePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WebHomePage extends StatelessWidget {
|
class WebHomePage extends StatelessWidget {
|
||||||
final connectionPage = ConnectionPage();
|
final connectionPage =
|
||||||
|
ConnectionPage(appBarActions: <Widget>[const WebSettingsPage()]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
stateGlobal.isInMainPage = true;
|
||||||
|
handleUnilink(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// backgroundColor: MyTheme.grayBg,
|
// backgroundColor: MyTheme.grayBg,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text(bind.mainGetAppNameSync()),
|
title: Text("${bind.mainGetAppNameSync()} (Preview)"),
|
||||||
actions: connectionPage.appBarActions,
|
actions: connectionPage.appBarActions,
|
||||||
),
|
),
|
||||||
body: connectionPage,
|
body: connectionPage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUnilink(BuildContext context) {
|
||||||
|
if (webInitialLink.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final link = webInitialLink;
|
||||||
|
webInitialLink = '';
|
||||||
|
final splitter = ["/#/", "/#", "#/", "#"];
|
||||||
|
var fakelink = '';
|
||||||
|
for (var s in splitter) {
|
||||||
|
if (link.contains(s)) {
|
||||||
|
var list = link.split(s);
|
||||||
|
if (list.length < 2 || list[1].isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.removeAt(0);
|
||||||
|
fakelink = "rustdesk://${list.join(s)}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fakelink.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final uri = Uri.tryParse(fakelink);
|
||||||
|
if (uri == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final args = urlLinkToCmdArgs(uri);
|
||||||
|
if (args == null || args.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool isFileTransfer = false;
|
||||||
|
String? id;
|
||||||
|
String? password;
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
switch (args[i]) {
|
||||||
|
case '--connect':
|
||||||
|
case '--play':
|
||||||
|
isFileTransfer = false;
|
||||||
|
id = args[i + 1];
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case '--file-transfer':
|
||||||
|
isFileTransfer = true;
|
||||||
|
id = args[i + 1];
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case '--password':
|
||||||
|
password = args[i + 1];
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (id != null) {
|
||||||
|
connect(context, id, isFileTransfer: isFileTransfer, password: password);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
|
|
||||||
final TextEditingController _textController =
|
final TextEditingController _textController =
|
||||||
TextEditingController(text: initText);
|
TextEditingController(text: initText);
|
||||||
|
// This timer is used to check the composing status of the soft keyboard.
|
||||||
|
// It is used for Android, Korean(and other similar) input method.
|
||||||
|
Timer? _composingTimer;
|
||||||
|
|
||||||
_RemotePageState(String id) {
|
_RemotePageState(String id) {
|
||||||
initSharedStates(id);
|
initSharedStates(id);
|
||||||
@@ -89,6 +92,13 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.chatModel
|
gFFI.chatModel
|
||||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||||
_blockableOverlayState.applyFfi(gFFI);
|
_blockableOverlayState.applyFfi(gFFI);
|
||||||
|
gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
|
gFFI.recordingModel
|
||||||
|
.updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
|
||||||
|
if (gFFI.recordingModel.start) {
|
||||||
|
showToast(translate('Automatically record outgoing sessions'));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -104,6 +114,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
_physicalFocusNode.dispose();
|
_physicalFocusNode.dispose();
|
||||||
await gFFI.close();
|
await gFFI.close();
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
_composingTimer?.cancel();
|
||||||
gFFI.dialogManager.dismissAll();
|
gFFI.dialogManager.dismissAll();
|
||||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
overlays: SystemUiOverlay.values);
|
overlays: SystemUiOverlay.values);
|
||||||
@@ -139,6 +150,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.ffiModel.pi.version.isNotEmpty) {
|
gFFI.ffiModel.pi.version.isNotEmpty) {
|
||||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||||
}
|
}
|
||||||
|
_composingTimer?.cancel();
|
||||||
} else {
|
} else {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||||
@@ -155,9 +167,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
var oldValue = _value;
|
var oldValue = _value;
|
||||||
_value = newValue;
|
_value = newValue;
|
||||||
var i = newValue.length - 1;
|
var i = newValue.length - 1;
|
||||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
for (; i >= 0 && newValue[i] != '1'; --i) {}
|
||||||
var j = oldValue.length - 1;
|
var j = oldValue.length - 1;
|
||||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
for (; j >= 0 && oldValue[j] != '1'; --j) {}
|
||||||
if (i < j) j = i;
|
if (i < j) j = i;
|
||||||
var subNewValue = newValue.substring(j + 1);
|
var subNewValue = newValue.substring(j + 1);
|
||||||
var subOldValue = oldValue.substring(j + 1);
|
var subOldValue = oldValue.substring(j + 1);
|
||||||
@@ -202,12 +214,19 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleNonIOSSoftKeyboardInput(String newValue) {
|
void _handleNonIOSSoftKeyboardInput(String newValue) {
|
||||||
|
_composingTimer?.cancel();
|
||||||
|
if (_textController.value.isComposingRangeValid) {
|
||||||
|
_composingTimer = Timer(Duration(milliseconds: 25), () {
|
||||||
|
_handleNonIOSSoftKeyboardInput(_textController.value.text);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
var oldValue = _value;
|
var oldValue = _value;
|
||||||
_value = newValue;
|
_value = newValue;
|
||||||
if (oldValue.isNotEmpty &&
|
if (oldValue.isNotEmpty &&
|
||||||
newValue.isNotEmpty &&
|
newValue.isNotEmpty &&
|
||||||
oldValue[0] == '\1' &&
|
oldValue[0] == '1' &&
|
||||||
newValue[0] != '\1') {
|
newValue[0] != '1') {
|
||||||
// clipboard
|
// clipboard
|
||||||
oldValue = '';
|
oldValue = '';
|
||||||
}
|
}
|
||||||
@@ -242,10 +261,14 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle mobile virtual keyboard
|
Future<void> handleSoftKeyboardInput(String newValue) async {
|
||||||
void handleSoftKeyboardInput(String newValue) {
|
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
_handleIOSSoftKeyboardInput(newValue);
|
// fix: TextFormField onChanged event triggered multiple times when Korean input
|
||||||
|
// https://github.com/rustdesk/rustdesk/pull/9644
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
if (newValue != _textController.text) return;
|
||||||
|
_handleIOSSoftKeyboardInput(_textController.text);
|
||||||
} else {
|
} else {
|
||||||
_handleNonIOSSoftKeyboardInput(newValue);
|
_handleNonIOSSoftKeyboardInput(newValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget {
|
|||||||
class _ScanPageState extends State<ScanPage> {
|
class _ScanPageState extends State<ScanPage> {
|
||||||
QRViewController? controller;
|
QRViewController? controller;
|
||||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||||
|
StreamSubscription? scanSubscription;
|
||||||
|
|
||||||
// In order to get hot reload to work we need to pause the camera if the platform
|
|
||||||
// is android, or resume the camera if the platform is iOS.
|
|
||||||
@override
|
@override
|
||||||
void reassemble() {
|
void reassemble() {
|
||||||
super.reassemble();
|
super.reassemble();
|
||||||
if (isAndroid) {
|
if (isAndroid && controller != null) {
|
||||||
controller!.pauseCamera();
|
controller!.pauseCamera();
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller!.resumeCamera();
|
||||||
}
|
}
|
||||||
controller!.resumeCamera();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Scan QR'),
|
title: const Text('Scan QR'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
_buildImagePickerButton(),
|
||||||
color: Colors.white,
|
_buildFlashToggleButton(),
|
||||||
icon: Icon(Icons.image_search),
|
_buildCameraSwitchButton(),
|
||||||
iconSize: 32.0,
|
],
|
||||||
onPressed: () async {
|
),
|
||||||
final ImagePicker picker = ImagePicker();
|
body: _buildQrView(context),
|
||||||
final XFile? file =
|
);
|
||||||
await picker.pickImage(source: ImageSource.gallery);
|
|
||||||
if (file != null) {
|
|
||||||
var image = img.decodeNamedImage(
|
|
||||||
file.path, File(file.path).readAsBytesSync())!;
|
|
||||||
|
|
||||||
LuminanceSource source = RGBLuminanceSource(
|
|
||||||
image.width,
|
|
||||||
image.height,
|
|
||||||
image
|
|
||||||
.getBytes(order: img.ChannelOrder.abgr)
|
|
||||||
.buffer
|
|
||||||
.asInt32List());
|
|
||||||
var bitmap = BinaryBitmap(HybridBinarizer(source));
|
|
||||||
|
|
||||||
var reader = QRCodeReader();
|
|
||||||
try {
|
|
||||||
var result = reader.decode(bitmap);
|
|
||||||
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
|
||||||
handleUriLink(uriString: result.text);
|
|
||||||
} else {
|
|
||||||
showServerSettingFromQr(result.text);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showToast('No QR code found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
IconButton(
|
|
||||||
color: Colors.yellow,
|
|
||||||
icon: Icon(Icons.flash_on),
|
|
||||||
iconSize: 32.0,
|
|
||||||
onPressed: () async {
|
|
||||||
await controller?.toggleFlash();
|
|
||||||
}),
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(Icons.switch_camera),
|
|
||||||
iconSize: 32.0,
|
|
||||||
onPressed: () async {
|
|
||||||
await controller?.flipCamera();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: _buildQrView(context));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQrView(BuildContext context) {
|
Widget _buildQrView(BuildContext context) {
|
||||||
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
|
var scanArea = MediaQuery.of(context).size.width < 400 ||
|
||||||
var scanArea = (MediaQuery.of(context).size.width < 400 ||
|
MediaQuery.of(context).size.height < 400
|
||||||
MediaQuery.of(context).size.height < 400)
|
|
||||||
? 150.0
|
? 150.0
|
||||||
: 300.0;
|
: 300.0;
|
||||||
// To ensure the Scanner view is properly sizes after rotation
|
|
||||||
// we need to listen for Flutter SizeChanged notification and update controller
|
|
||||||
return QRView(
|
return QRView(
|
||||||
key: qrKey,
|
key: qrKey,
|
||||||
onQRViewCreated: _onQRViewCreated,
|
onQRViewCreated: _onQRViewCreated,
|
||||||
overlay: QrScannerOverlayShape(
|
overlay: QrScannerOverlayShape(
|
||||||
borderColor: Colors.red,
|
borderColor: Colors.red,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
borderLength: 30,
|
borderLength: 30,
|
||||||
borderWidth: 10,
|
borderWidth: 10,
|
||||||
cutOutSize: scanArea),
|
cutOutSize: scanArea,
|
||||||
|
),
|
||||||
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
|
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -116,7 +69,7 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
});
|
});
|
||||||
controller.scannedDataStream.listen((scanData) {
|
scanSubscription = controller.scannedDataStream.listen((scanData) {
|
||||||
if (scanData.code != null) {
|
if (scanData.code != null) {
|
||||||
showServerSettingFromQr(scanData.code!);
|
showServerSettingFromQr(scanData.code!);
|
||||||
}
|
}
|
||||||
@@ -129,8 +82,66 @@ class _ScanPageState extends State<ScanPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pickImage() async {
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
final XFile? file = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
if (file != null) {
|
||||||
|
try {
|
||||||
|
var image = img.decodeImage(await File(file.path).readAsBytes())!;
|
||||||
|
LuminanceSource source = RGBLuminanceSource(
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(),
|
||||||
|
);
|
||||||
|
var bitmap = BinaryBitmap(HybridBinarizer(source));
|
||||||
|
|
||||||
|
var reader = QRCodeReader();
|
||||||
|
var result = reader.decode(bitmap);
|
||||||
|
if (result.text.startsWith(bind.mainUriPrefixSync())) {
|
||||||
|
handleUriLink(uriString: result.text);
|
||||||
|
} else {
|
||||||
|
showServerSettingFromQr(result.text);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('No QR code found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagePickerButton() {
|
||||||
|
return IconButton(
|
||||||
|
color: Colors.white,
|
||||||
|
icon: Icon(Icons.image_search),
|
||||||
|
iconSize: 32.0,
|
||||||
|
onPressed: _pickImage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFlashToggleButton() {
|
||||||
|
return IconButton(
|
||||||
|
color: Colors.yellow,
|
||||||
|
icon: Icon(Icons.flash_on),
|
||||||
|
iconSize: 32.0,
|
||||||
|
onPressed: () async {
|
||||||
|
await controller?.toggleFlash();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCameraSwitchButton() {
|
||||||
|
return IconButton(
|
||||||
|
color: Colors.white,
|
||||||
|
icon: Icon(Icons.switch_camera),
|
||||||
|
iconSize: 32.0,
|
||||||
|
onPressed: () async {
|
||||||
|
await controller?.flipCamera();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
scanSubscription?.cancel();
|
||||||
controller?.dispose();
|
controller?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _enableRecordSession = false;
|
var _enableRecordSession = false;
|
||||||
var _enableHardwareCodec = false;
|
var _enableHardwareCodec = false;
|
||||||
var _autoRecordIncomingSession = false;
|
var _autoRecordIncomingSession = false;
|
||||||
|
var _autoRecordOutgoingSession = false;
|
||||||
var _allowAutoDisconnect = false;
|
var _allowAutoDisconnect = false;
|
||||||
var _localIP = "";
|
var _localIP = "";
|
||||||
var _directAccessPort = "";
|
var _directAccessPort = "";
|
||||||
@@ -104,6 +105,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
||||||
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
||||||
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
||||||
|
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
|
||||||
|
bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing));
|
||||||
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
||||||
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
||||||
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||||
@@ -231,6 +234,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Provider.of<FfiModel>(context);
|
Provider.of<FfiModel>(context);
|
||||||
final outgoingOnly = bind.isOutgoingOnly();
|
final outgoingOnly = bind.isOutgoingOnly();
|
||||||
|
final incommingOnly = bind.isIncomingOnly();
|
||||||
final customClientSection = CustomSettingsSection(
|
final customClientSection = CustomSettingsSection(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -674,32 +678,55 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
if (isAndroid && !outgoingOnly)
|
if (isAndroid)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Recording")),
|
title: Text(translate("Recording")),
|
||||||
tiles: [
|
tiles: [
|
||||||
SettingsTile.switchTile(
|
if (!outgoingOnly)
|
||||||
title:
|
SettingsTile.switchTile(
|
||||||
Text(translate('Automatically record incoming sessions')),
|
title:
|
||||||
leading: Icon(Icons.videocam),
|
Text(translate('Automatically record incoming sessions')),
|
||||||
description: Text(
|
initialValue: _autoRecordIncomingSession,
|
||||||
"${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
|
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||||
initialValue: _autoRecordIncomingSession,
|
? null
|
||||||
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
: (v) async {
|
||||||
? null
|
await bind.mainSetOption(
|
||||||
: (v) async {
|
key: kOptionAllowAutoRecordIncoming,
|
||||||
await bind.mainSetOption(
|
value: bool2option(
|
||||||
key: kOptionAllowAutoRecordIncoming,
|
kOptionAllowAutoRecordIncoming, v));
|
||||||
value:
|
final newValue = option2bool(
|
||||||
bool2option(kOptionAllowAutoRecordIncoming, v));
|
kOptionAllowAutoRecordIncoming,
|
||||||
final newValue = option2bool(
|
await bind.mainGetOption(
|
||||||
kOptionAllowAutoRecordIncoming,
|
key: kOptionAllowAutoRecordIncoming));
|
||||||
await bind.mainGetOption(
|
setState(() {
|
||||||
key: kOptionAllowAutoRecordIncoming));
|
_autoRecordIncomingSession = newValue;
|
||||||
setState(() {
|
});
|
||||||
_autoRecordIncomingSession = newValue;
|
},
|
||||||
});
|
),
|
||||||
},
|
if (!incommingOnly)
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title:
|
||||||
|
Text(translate('Automatically record outgoing sessions')),
|
||||||
|
initialValue: _autoRecordOutgoingSession,
|
||||||
|
onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
|
await bind.mainSetLocalOption(
|
||||||
|
key: kOptionAllowAutoRecordOutgoing,
|
||||||
|
value: bool2option(
|
||||||
|
kOptionAllowAutoRecordOutgoing, v));
|
||||||
|
final newValue = option2bool(
|
||||||
|
kOptionAllowAutoRecordOutgoing,
|
||||||
|
bind.mainGetLocalOption(
|
||||||
|
key: kOptionAllowAutoRecordOutgoing));
|
||||||
|
setState(() {
|
||||||
|
_autoRecordOutgoingSession = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SettingsTile(
|
||||||
|
title: Text(translate("Directory")),
|
||||||
|
description: Text(bind.mainVideoSaveDirectory(root: false)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -205,14 +205,15 @@ void showServerSettingsWithValue(
|
|||||||
)
|
)
|
||||||
] +
|
] +
|
||||||
[
|
[
|
||||||
TextFormField(
|
if (isAndroid)
|
||||||
controller: relayCtrl,
|
TextFormField(
|
||||||
decoration: InputDecoration(
|
controller: relayCtrl,
|
||||||
labelText: translate('Relay Server'),
|
decoration: InputDecoration(
|
||||||
errorText: relayServerMsg.value.isEmpty
|
labelText: translate('Relay Server'),
|
||||||
? null
|
errorText: relayServerMsg.value.isEmpty
|
||||||
: relayServerMsg.value),
|
? null
|
||||||
)
|
: relayServerMsg.value),
|
||||||
|
)
|
||||||
] +
|
] +
|
||||||
[
|
[
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
|||||||
@@ -66,10 +66,16 @@ class AbModel {
|
|||||||
var listInitialized = false;
|
var listInitialized = false;
|
||||||
var _maxPeerOneAb = 0;
|
var _maxPeerOneAb = 0;
|
||||||
|
|
||||||
|
late final Peers peersModel;
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
AbModel(this.parent) {
|
AbModel(this.parent) {
|
||||||
addressbooks.clear();
|
addressbooks.clear();
|
||||||
|
peersModel = Peers(
|
||||||
|
name: PeersModelName.addressBook,
|
||||||
|
getInitPeers: () => currentAbPeers,
|
||||||
|
loadEvent: LoadEvent.addressBook);
|
||||||
if (desktopType == DesktopType.main) {
|
if (desktopType == DesktopType.main) {
|
||||||
Timer.periodic(Duration(milliseconds: 500), (timer) async {
|
Timer.periodic(Duration(milliseconds: 500), (timer) async {
|
||||||
if (_timerCounter++ % 6 == 0) {
|
if (_timerCounter++ % 6 == 0) {
|
||||||
|
|||||||
@@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) ||
|
_isChatOverlayHide() =>
|
||||||
chatWindowOverlayEntry == null);
|
((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) ||
|
||||||
|
chatWindowOverlayEntry == null);
|
||||||
|
|
||||||
toggleChatOverlay({Offset? chatInitPos}) {
|
toggleChatOverlay({Offset? chatInitPos}) {
|
||||||
if (_isChatOverlayHide()) {
|
if (_isChatOverlayHide()) {
|
||||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
if (!isDesktop) {
|
if (!(isDesktop || isWebDesktop)) {
|
||||||
showChatIconOverlay();
|
showChatIconOverlay();
|
||||||
}
|
}
|
||||||
showChatWindowOverlay(chatInitPos: chatInitPos);
|
showChatWindowOverlay(chatInitPos: chatInitPos);
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ class TextureModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentDisplay(int curDisplay) {
|
updateCurrentDisplay(int curDisplay) {
|
||||||
|
if (isWeb) return;
|
||||||
final ffi = parent.target;
|
final ffi = parent.target;
|
||||||
if (ffi == null) return;
|
if (ffi == null) return;
|
||||||
tryCreateTexture(int idx) {
|
tryCreateTexture(int idx) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
|||||||
import 'package:flutter_hbb/utils/event_loop.dart';
|
import 'package:flutter_hbb/utils/event_loop.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:flutter_hbb/web/dummy.dart'
|
||||||
|
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
|
||||||
|
|
||||||
import '../consts.dart';
|
import '../consts.dart';
|
||||||
import 'model.dart';
|
import 'model.dart';
|
||||||
@@ -34,6 +36,7 @@ class JobID {
|
|||||||
}
|
}
|
||||||
|
|
||||||
typedef GetSessionID = SessionID Function();
|
typedef GetSessionID = SessionID Function();
|
||||||
|
typedef GetDialogManager = OverlayDialogManager? Function();
|
||||||
|
|
||||||
class FileModel {
|
class FileModel {
|
||||||
final WeakReference<FFI> parent;
|
final WeakReference<FFI> parent;
|
||||||
@@ -45,13 +48,15 @@ class FileModel {
|
|||||||
late final FileController remoteController;
|
late final FileController remoteController;
|
||||||
|
|
||||||
late final GetSessionID getSessionID;
|
late final GetSessionID getSessionID;
|
||||||
|
late final GetDialogManager getDialogManager;
|
||||||
SessionID get sessionId => getSessionID();
|
SessionID get sessionId => getSessionID();
|
||||||
late final FileDialogEventLoop evtLoop;
|
late final FileDialogEventLoop evtLoop;
|
||||||
|
|
||||||
FileModel(this.parent) {
|
FileModel(this.parent) {
|
||||||
getSessionID = () => parent.target!.sessionId;
|
getSessionID = () => parent.target!.sessionId;
|
||||||
|
getDialogManager = () => parent.target?.dialogManager;
|
||||||
fileFetcher = FileFetcher(getSessionID);
|
fileFetcher = FileFetcher(getSessionID);
|
||||||
jobController = JobController(getSessionID);
|
jobController = JobController(getSessionID, getDialogManager);
|
||||||
localController = FileController(
|
localController = FileController(
|
||||||
isLocal: true,
|
isLocal: true,
|
||||||
getSessionID: getSessionID,
|
getSessionID: getSessionID,
|
||||||
@@ -71,7 +76,7 @@ class FileModel {
|
|||||||
|
|
||||||
Future<void> onReady() async {
|
Future<void> onReady() async {
|
||||||
await evtLoop.onReady();
|
await evtLoop.onReady();
|
||||||
await localController.onReady();
|
if (!isWeb) await localController.onReady();
|
||||||
await remoteController.onReady();
|
await remoteController.onReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +88,7 @@ class FileModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshAll() async {
|
Future<void> refreshAll() async {
|
||||||
await localController.refresh();
|
if (!isWeb) await localController.refresh();
|
||||||
await remoteController.refresh();
|
await remoteController.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +230,33 @@ class FileModel {
|
|||||||
);
|
);
|
||||||
}, useAnimation: false);
|
}, useAnimation: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onSelectedFiles(dynamic obj) {
|
||||||
|
localController.selectedItems.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
int handleIndex = int.parse(obj['handleIndex']);
|
||||||
|
final file = jsonDecode(obj['file']);
|
||||||
|
var entry = Entry.fromJson(file);
|
||||||
|
entry.path = entry.name;
|
||||||
|
final otherSideData = remoteController.directoryData();
|
||||||
|
final toPath = otherSideData.directory.path;
|
||||||
|
final isWindows = otherSideData.options.isWindows;
|
||||||
|
final showHidden = otherSideData.options.showHidden;
|
||||||
|
final jobID = jobController.addTransferJob(entry, false);
|
||||||
|
webSendLocalFiles(
|
||||||
|
handleIndex: handleIndex,
|
||||||
|
actId: jobID,
|
||||||
|
path: entry.path,
|
||||||
|
to: PathUtil.join(toPath, entry.name, isWindows),
|
||||||
|
fileNum: 0,
|
||||||
|
includeHidden: showHidden,
|
||||||
|
isRemote: false,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to decode onSelectedFiles: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DirectoryData {
|
class DirectoryData {
|
||||||
@@ -451,7 +483,7 @@ class FileController {
|
|||||||
final isWindows = otherSideData.options.isWindows;
|
final isWindows = otherSideData.options.isWindows;
|
||||||
final showHidden = otherSideData.options.showHidden;
|
final showHidden = otherSideData.options.showHidden;
|
||||||
for (var from in items.items) {
|
for (var from in items.items) {
|
||||||
final jobID = jobController.add(from, isRemoteToLocal);
|
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
|
||||||
bind.sessionSendFiles(
|
bind.sessionSendFiles(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
actId: jobID,
|
actId: jobID,
|
||||||
@@ -459,7 +491,8 @@ class FileController {
|
|||||||
to: PathUtil.join(toPath, from.name, isWindows),
|
to: PathUtil.join(toPath, from.name, isWindows),
|
||||||
fileNum: 0,
|
fileNum: 0,
|
||||||
includeHidden: showHidden,
|
includeHidden: showHidden,
|
||||||
isRemote: isRemoteToLocal);
|
isRemote: isRemoteToLocal,
|
||||||
|
isDir: from.isDirectory);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
|
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
|
||||||
}
|
}
|
||||||
@@ -486,7 +519,7 @@ class FileController {
|
|||||||
} else if (item.isDirectory) {
|
} else if (item.isDirectory) {
|
||||||
title = translate("Not an empty directory");
|
title = translate("Not an empty directory");
|
||||||
dialogManager?.showLoading(translate("Waiting"));
|
dialogManager?.showLoading(translate("Waiting"));
|
||||||
final fd = await fileFetcher.fetchDirectoryRecursive(
|
final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||||
jobID, item.path, items.isLocal, true);
|
jobID, item.path, items.isLocal, true);
|
||||||
if (fd.path.isEmpty) {
|
if (fd.path.isEmpty) {
|
||||||
fd.path = item.path;
|
fd.path = item.path;
|
||||||
@@ -494,13 +527,21 @@ class FileController {
|
|||||||
fd.format(isWindows);
|
fd.format(isWindows);
|
||||||
dialogManager?.dismissAll();
|
dialogManager?.dismissAll();
|
||||||
if (fd.entries.isEmpty) {
|
if (fd.entries.isEmpty) {
|
||||||
|
var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
|
||||||
final confirm = await showRemoveDialog(
|
final confirm = await showRemoveDialog(
|
||||||
translate(
|
translate(
|
||||||
"Are you sure you want to delete this empty directory?"),
|
"Are you sure you want to delete this empty directory?"),
|
||||||
item.name,
|
item.name,
|
||||||
false);
|
false);
|
||||||
if (confirm == true) {
|
if (confirm == true) {
|
||||||
sendRemoveEmptyDir(item.path, 0);
|
sendRemoveEmptyDir(
|
||||||
|
item.path,
|
||||||
|
0,
|
||||||
|
deleteJobId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
jobController.updateJobStatus(deleteJobId,
|
||||||
|
error: "cancel", state: JobState.done);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -508,6 +549,13 @@ class FileController {
|
|||||||
} else {
|
} else {
|
||||||
entries = [];
|
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++) {
|
for (var i = 0; i < entries.length; i++) {
|
||||||
final dirShow = item.isDirectory
|
final dirShow = item.isDirectory
|
||||||
@@ -522,24 +570,32 @@ class FileController {
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
if (confirm == true) {
|
if (confirm == true) {
|
||||||
sendRemoveFile(entries[i].path, i);
|
sendRemoveFile(entries[i].path, i, deleteJobId);
|
||||||
final res = await jobController.jobResultListener.start();
|
final res = await jobController.jobResultListener.start();
|
||||||
// handle remove res;
|
// handle remove res;
|
||||||
if (item.isDirectory &&
|
if (item.isDirectory &&
|
||||||
res['file_num'] == (entries.length - 1).toString()) {
|
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 (_removeCheckboxRemember) {
|
||||||
if (confirm == true) {
|
if (confirm == true) {
|
||||||
for (var j = i + 1; j < entries.length; j++) {
|
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();
|
final res = await jobController.jobResultListener.start();
|
||||||
if (item.isDirectory &&
|
if (item.isDirectory &&
|
||||||
res['file_num'] == (entries.length - 1).toString()) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
@@ -618,22 +674,19 @@ class FileController {
|
|||||||
}, useAnimation: false);
|
}, useAnimation: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendRemoveFile(String path, int fileNum) {
|
void sendRemoveFile(String path, int fileNum, int actId) {
|
||||||
bind.sessionRemoveFile(
|
bind.sessionRemoveFile(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
actId: JobController.jobID.next(),
|
actId: actId,
|
||||||
path: path,
|
path: path,
|
||||||
isRemote: !isLocal,
|
isRemote: !isLocal,
|
||||||
fileNum: fileNum);
|
fileNum: fileNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendRemoveEmptyDir(String path, int fileNum) {
|
void sendRemoveEmptyDir(String path, int fileNum, int actId) {
|
||||||
history.removeWhere((element) => element.contains(path));
|
history.removeWhere((element) => element.contains(path));
|
||||||
bind.sessionRemoveAllEmptyDirs(
|
bind.sessionRemoveAllEmptyDirs(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
|
||||||
actId: JobController.jobID.next(),
|
|
||||||
path: path,
|
|
||||||
isRemote: !isLocal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createDir(String path) async {
|
Future<void> createDir(String path) async {
|
||||||
@@ -716,27 +769,29 @@ class FileController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _kOneWayFileTransferError = 'one-way-file-transfer-tip';
|
||||||
|
|
||||||
class JobController {
|
class JobController {
|
||||||
static final JobID jobID = JobID();
|
static final JobID jobID = JobID();
|
||||||
final jobTable = List<JobProgress>.empty(growable: true).obs;
|
final jobTable = List<JobProgress>.empty(growable: true).obs;
|
||||||
final jobResultListener = JobResultListener<Map<String, dynamic>>();
|
final jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||||
final GetSessionID getSessionID;
|
final GetSessionID getSessionID;
|
||||||
|
final GetDialogManager getDialogManager;
|
||||||
SessionID get sessionId => getSessionID();
|
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) {
|
int getJob(int id) {
|
||||||
return jobTable.indexWhere((element) => element.id == id);
|
return jobTable.indexWhere((element) => element.id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobProgress? getJob(int id) {
|
|
||||||
// return jobTable.firstWhere((element) => element.id == id);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return jobID
|
// return jobID
|
||||||
int add(Entry from, bool isRemoteToLocal) {
|
int addTransferJob(Entry from, bool isRemoteToLocal) {
|
||||||
final jobID = JobController.jobID.next();
|
final jobID = JobController.jobID.next();
|
||||||
jobTable.add(JobProgress()
|
jobTable.add(JobProgress()
|
||||||
|
..type = JobType.transfer
|
||||||
..fileName = path.basename(from.path)
|
..fileName = path.basename(from.path)
|
||||||
..jobName = from.path
|
..jobName = from.path
|
||||||
..totalSize = from.size
|
..totalSize = from.size
|
||||||
@@ -746,6 +801,33 @@ class JobController {
|
|||||||
return jobID;
|
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) {
|
void tryUpdateJobProgress(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
int id = int.parse(evt['id']);
|
int id = int.parse(evt['id']);
|
||||||
@@ -756,7 +838,7 @@ class JobController {
|
|||||||
job.fileNum = int.parse(evt['file_num']);
|
job.fileNum = int.parse(evt['file_num']);
|
||||||
job.speed = double.parse(evt['speed']);
|
job.speed = double.parse(evt['speed']);
|
||||||
job.finishedSize = int.parse(evt['finished_size']);
|
job.finishedSize = int.parse(evt['finished_size']);
|
||||||
debugPrint("update job $id with $evt");
|
job.recvJobRes = true;
|
||||||
jobTable.refresh();
|
jobTable.refresh();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -764,20 +846,48 @@ class JobController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void jobDone(Map<String, dynamic> evt) async {
|
Future<bool> jobDone(Map<String, dynamic> evt) async {
|
||||||
if (jobResultListener.isListening) {
|
if (jobResultListener.isListening) {
|
||||||
jobResultListener.complete(evt);
|
jobResultListener.complete(evt);
|
||||||
return;
|
// return;
|
||||||
}
|
}
|
||||||
|
int id = -1;
|
||||||
int id = int.parse(evt['id']);
|
int? fileNum = 0;
|
||||||
|
double? speed = 0;
|
||||||
|
try {
|
||||||
|
id = int.parse(evt['id']);
|
||||||
|
} catch (_) {}
|
||||||
final jobIndex = getJob(id);
|
final jobIndex = getJob(id);
|
||||||
if (jobIndex != -1) {
|
if (jobIndex == -1) return true;
|
||||||
final job = jobTable[jobIndex];
|
final job = jobTable[jobIndex];
|
||||||
job.finishedSize = job.totalSize;
|
job.recvJobRes = true;
|
||||||
|
if (job.type == JobType.deleteFile) {
|
||||||
job.state = JobState.done;
|
job.state = JobState.done;
|
||||||
job.fileNum = int.parse(evt['file_num']);
|
} else if (job.type == JobType.deleteDir) {
|
||||||
jobTable.refresh();
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,16 +898,61 @@ class JobController {
|
|||||||
final job = jobTable[jobIndex];
|
final job = jobTable[jobIndex];
|
||||||
job.state = JobState.error;
|
job.state = JobState.error;
|
||||||
job.err = err;
|
job.err = err;
|
||||||
job.fileNum = int.parse(evt['file_num']);
|
job.recvJobRes = true;
|
||||||
if (err == "skipped") {
|
if (job.type == JobType.transfer) {
|
||||||
job.state = JobState.done;
|
int? fileNum = int.tryParse(evt['file_num']);
|
||||||
job.finishedSize = job.totalSize;
|
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();
|
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");
|
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 {
|
Future<void> cancelJob(int id) async {
|
||||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||||
}
|
}
|
||||||
@@ -814,6 +969,7 @@ class JobController {
|
|||||||
final currJobId = JobController.jobID.next();
|
final currJobId = JobController.jobID.next();
|
||||||
String fileName = path.basename(isRemote ? remote : to);
|
String fileName = path.basename(isRemote ? remote : to);
|
||||||
var jobProgress = JobProgress()
|
var jobProgress = JobProgress()
|
||||||
|
..type = JobType.transfer
|
||||||
..fileName = fileName
|
..fileName = fileName
|
||||||
..jobName = isRemote ? remote : to
|
..jobName = isRemote ? remote : to
|
||||||
..id = currJobId
|
..id = currJobId
|
||||||
@@ -989,11 +1145,11 @@ class FileFetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FileDirectory> fetchDirectoryRecursive(
|
Future<FileDirectory> fetchDirectoryRecursiveToRemove(
|
||||||
int actID, String path, bool isLocal, bool showHidden) async {
|
int actID, String path, bool isLocal, bool showHidden) async {
|
||||||
// TODO test Recursive is show hidden default?
|
// TODO test Recursive is show hidden default?
|
||||||
try {
|
try {
|
||||||
await bind.sessionReadDirRecursive(
|
await bind.sessionReadDirToRemoveRecursive(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
actId: actID,
|
actId: actID,
|
||||||
path: path,
|
path: path,
|
||||||
@@ -1088,8 +1244,12 @@ extension JobStateDisplay on JobState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum JobType { none, transfer, deleteFile, deleteDir }
|
||||||
|
|
||||||
class JobProgress {
|
class JobProgress {
|
||||||
|
JobType type = JobType.none;
|
||||||
JobState state = JobState.none;
|
JobState state = JobState.none;
|
||||||
|
var recvJobRes = false;
|
||||||
var id = 0;
|
var id = 0;
|
||||||
var fileNum = 0;
|
var fileNum = 0;
|
||||||
var speed = 0.0;
|
var speed = 0.0;
|
||||||
@@ -1109,7 +1269,9 @@ class JobProgress {
|
|||||||
int lastTransferredSize = 0;
|
int lastTransferredSize = 0;
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
type = JobType.none;
|
||||||
state = JobState.none;
|
state = JobState.none;
|
||||||
|
recvJobRes = false;
|
||||||
id = 0;
|
id = 0;
|
||||||
fileNum = 0;
|
fileNum = 0;
|
||||||
speed = 0;
|
speed = 0;
|
||||||
@@ -1123,11 +1285,81 @@ class JobProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String display() {
|
String display() {
|
||||||
if (state == JobState.done && err == "skipped") {
|
if (type == JobType.transfer) {
|
||||||
return translate("Skipped");
|
if (state == JobState.done && err == "skipped") {
|
||||||
|
return translate("Skipped");
|
||||||
|
}
|
||||||
|
} else if (type == JobType.deleteFile) {
|
||||||
|
if (err == "cancel") {
|
||||||
|
return translate("Cancel");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.display();
|
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 {
|
class _PathStat {
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ class GroupModel {
|
|||||||
|
|
||||||
bool get emtpy => users.isEmpty && peers.isEmpty;
|
bool get emtpy => users.isEmpty && peers.isEmpty;
|
||||||
|
|
||||||
GroupModel(this.parent);
|
late final Peers peersModel;
|
||||||
|
|
||||||
|
GroupModel(this.parent) {
|
||||||
|
peersModel = Peers(
|
||||||
|
name: PeersModelName.group,
|
||||||
|
getInitPeers: () => peers,
|
||||||
|
loadEvent: LoadEvent.group);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> pull({force = true, quiet = false}) async {
|
Future<void> pull({force = true, quiet = false}) async {
|
||||||
if (bind.isDisableGroupPanel()) return;
|
if (bind.isDisableGroupPanel()) return;
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class PointerEventToRust {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToReleaseKeys {
|
class ToReleaseRawKeys {
|
||||||
RawKeyEvent? lastLShiftKeyEvent;
|
RawKeyEvent? lastLShiftKeyEvent;
|
||||||
RawKeyEvent? lastRShiftKeyEvent;
|
RawKeyEvent? lastRShiftKeyEvent;
|
||||||
RawKeyEvent? lastLCtrlKeyEvent;
|
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 {
|
class InputModel {
|
||||||
final WeakReference<FFI> parent;
|
final WeakReference<FFI> parent;
|
||||||
String keyboardMode = '';
|
String keyboardMode = '';
|
||||||
@@ -292,6 +334,7 @@ class InputModel {
|
|||||||
var alt = false;
|
var alt = false;
|
||||||
var command = false;
|
var command = false;
|
||||||
|
|
||||||
|
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
|
||||||
final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
|
final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
|
||||||
|
|
||||||
// trackpad
|
// trackpad
|
||||||
@@ -339,10 +382,99 @@ 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) {
|
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||||
if (isViewOnly) return KeyEventResult.handled;
|
if (isViewOnly) return KeyEventResult.handled;
|
||||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
if (!isInputSourceFlutter) {
|
||||||
return KeyEventResult.handled;
|
if (isDesktop) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else if (isWeb) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final key = e.logicalKey;
|
final key = e.logicalKey;
|
||||||
@@ -358,7 +490,7 @@ class InputModel {
|
|||||||
command = true;
|
command = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toReleaseKeys.updateKeyDown(key, e);
|
toReleaseRawKeys.updateKeyDown(key, e);
|
||||||
}
|
}
|
||||||
if (e is RawKeyUpEvent) {
|
if (e is RawKeyUpEvent) {
|
||||||
if (key == LogicalKeyboardKey.altLeft ||
|
if (key == LogicalKeyboardKey.altLeft ||
|
||||||
@@ -376,12 +508,49 @@ class InputModel {
|
|||||||
command = false;
|
command = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
toReleaseKeys.updateKeyUp(key, e);
|
toReleaseRawKeys.updateKeyUp(key, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// * Currently mobile does not enable map mode
|
// * Currently mobile does not enable map mode
|
||||||
if ((isDesktop || isWebDesktop) && keyboardMode == 'map') {
|
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||||
mapKeyboardMode(e);
|
mapKeyboardModeRaw(e);
|
||||||
|
} else {
|
||||||
|
legacyKeyboardModeRaw(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEventResult handleKeyEvent(KeyEvent e) {
|
||||||
|
if (isViewOnly) return KeyEventResult.handled;
|
||||||
|
if (!isInputSourceFlutter) {
|
||||||
|
if (isDesktop) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else if (isWeb) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile || (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 {
|
} else {
|
||||||
legacyKeyboardMode(e);
|
legacyKeyboardMode(e);
|
||||||
}
|
}
|
||||||
@@ -389,7 +558,33 @@ class InputModel {
|
|||||||
return KeyEventResult.handled;
|
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 positionCode = -1;
|
||||||
int platformCode = -1;
|
int platformCode = -1;
|
||||||
bool down;
|
bool down;
|
||||||
@@ -441,7 +636,7 @@ class InputModel {
|
|||||||
.contains(KeyboardLockMode.scrollLock)) {
|
.contains(KeyboardLockMode.scrollLock)) {
|
||||||
lockModes |= (1 << scrolllock);
|
lockModes |= (1 << scrolllock);
|
||||||
}
|
}
|
||||||
bind.sessionHandleFlutterKeyEvent(
|
bind.sessionHandleFlutterRawKeyEvent(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
name: name,
|
name: name,
|
||||||
platformCode: platformCode,
|
platformCode: platformCode,
|
||||||
@@ -450,7 +645,7 @@ class InputModel {
|
|||||||
downOrUp: down);
|
downOrUp: down);
|
||||||
}
|
}
|
||||||
|
|
||||||
void legacyKeyboardMode(RawKeyEvent e) {
|
void legacyKeyboardModeRaw(RawKeyEvent e) {
|
||||||
if (e is RawKeyDownEvent) {
|
if (e is RawKeyDownEvent) {
|
||||||
if (e.repeat) {
|
if (e.repeat) {
|
||||||
sendRawKey(e, press: true);
|
sendRawKey(e, press: true);
|
||||||
@@ -471,6 +666,24 @@ class InputModel {
|
|||||||
inputKey(label, down: down, press: press ?? false);
|
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.
|
/// Send key stroke event.
|
||||||
/// [down] indicates the key's state(down or up).
|
/// [down] indicates the key's state(down or up).
|
||||||
/// [press] indicates a click event(down and up).
|
/// [press] indicates a click event(down and up).
|
||||||
@@ -566,7 +779,8 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void enterOrLeave(bool enter) {
|
void enterOrLeave(bool enter) {
|
||||||
toReleaseKeys.release(handleRawKeyEvent);
|
toReleaseKeys.release(handleKeyEvent);
|
||||||
|
toReleaseRawKeys.release(handleRawKeyEvent);
|
||||||
_pointerMovedAfterEnter = false;
|
_pointerMovedAfterEnter = false;
|
||||||
|
|
||||||
// Fix status
|
// Fix status
|
||||||
@@ -577,6 +791,9 @@ class InputModel {
|
|||||||
if (!isInputSourceFlutter) {
|
if (!isInputSourceFlutter) {
|
||||||
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
|
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
|
||||||
}
|
}
|
||||||
|
if (!isWeb && enter) {
|
||||||
|
bind.setCurSessionId(sessionId: sessionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send mouse movement event with distance in [x] and [y].
|
/// Send mouse movement event with distance in [x] and [y].
|
||||||
@@ -1164,15 +1381,15 @@ class InputModel {
|
|||||||
// Simulate a key press event.
|
// Simulate a key press event.
|
||||||
// `usbHidUsage` is the USB HID usage code of the key.
|
// `usbHidUsage` is the USB HID usage code of the key.
|
||||||
Future<void> tapHidKey(int usbHidUsage) async {
|
Future<void> tapHidKey(int usbHidUsage) async {
|
||||||
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true);
|
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false);
|
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onMobileVolumeUp() async =>
|
Future<void> onMobileVolumeUp() async =>
|
||||||
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage);
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF);
|
||||||
Future<void> onMobileVolumeDown() async =>
|
Future<void> onMobileVolumeDown() async =>
|
||||||
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage);
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF);
|
||||||
Future<void> onMobilePower() async =>
|
Future<void> onMobilePower() async =>
|
||||||
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage);
|
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ import 'dart:math';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/ab_model.dart';
|
import 'package:flutter_hbb/models/ab_model.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/cm_file_model.dart';
|
import 'package:flutter_hbb/models/cm_file_model.dart';
|
||||||
import 'package:flutter_hbb/models/file_model.dart';
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
import 'package:flutter_hbb/models/group_model.dart';
|
import 'package:flutter_hbb/models/group_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
import 'package:flutter_hbb/models/user_model.dart';
|
import 'package:flutter_hbb/models/user_model.dart';
|
||||||
@@ -267,6 +270,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
var name = evt['name'];
|
var name = evt['name'];
|
||||||
if (name == 'msgbox') {
|
if (name == 'msgbox') {
|
||||||
handleMsgBox(evt, sessionId, peerId);
|
handleMsgBox(evt, sessionId, peerId);
|
||||||
|
} else if (name == 'toast') {
|
||||||
|
handleToast(evt, sessionId, peerId);
|
||||||
} else if (name == 'set_multiple_windows_session') {
|
} else if (name == 'set_multiple_windows_session') {
|
||||||
handleMultipleWindowsSession(evt, sessionId, peerId);
|
handleMultipleWindowsSession(evt, sessionId, peerId);
|
||||||
} else if (name == 'peer_info') {
|
} else if (name == 'peer_info') {
|
||||||
@@ -304,8 +309,13 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'job_progress') {
|
} else if (name == 'job_progress') {
|
||||||
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
|
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
|
||||||
} else if (name == 'job_done') {
|
} else if (name == 'job_done') {
|
||||||
parent.target?.fileModel.jobController.jobDone(evt);
|
bool? refresh =
|
||||||
parent.target?.fileModel.refreshAll();
|
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') {
|
} else if (name == 'job_error') {
|
||||||
parent.target?.fileModel.jobController.jobError(evt);
|
parent.target?.fileModel.jobController.jobError(evt);
|
||||||
} else if (name == 'override_file_confirm') {
|
} else if (name == 'override_file_confirm') {
|
||||||
@@ -365,7 +375,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'plugin_option') {
|
} else if (name == 'plugin_option') {
|
||||||
handleOption(evt);
|
handleOption(evt);
|
||||||
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
} else if (name == "sync_peer_hash_password_to_personal_ab") {
|
||||||
if (desktopType == DesktopType.main) {
|
if (desktopType == DesktopType.main || isWeb || isMobile) {
|
||||||
final id = evt['id'];
|
final id = evt['id'];
|
||||||
final hash = evt['hash'];
|
final hash = evt['hash'];
|
||||||
if (id != null && hash != null) {
|
if (id != null && hash != null) {
|
||||||
@@ -383,6 +393,14 @@ class FfiModel with ChangeNotifier {
|
|||||||
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
||||||
} else if (name == 'use_texture_render') {
|
} else if (name == 'use_texture_render') {
|
||||||
_handleUseTextureRender(evt, sessionId, peerId);
|
_handleUseTextureRender(evt, sessionId, peerId);
|
||||||
|
} else if (name == "selected_files") {
|
||||||
|
if (isWeb) {
|
||||||
|
parent.target?.fileModel.onSelectedFiles(evt);
|
||||||
|
}
|
||||||
|
} else if (name == "record_status") {
|
||||||
|
if (desktopType == DesktopType.remote || isMobile) {
|
||||||
|
parent.target?.recordingModel.updateStatus(evt['start'] == 'true');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Event is not handled in the fixed branch: $name');
|
debugPrint('Event is not handled in the fixed branch: $name');
|
||||||
}
|
}
|
||||||
@@ -492,10 +510,12 @@ class FfiModel with ChangeNotifier {
|
|||||||
newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
|
newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
|
||||||
newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
|
newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
|
||||||
newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
|
newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
|
||||||
newDisplay.originalWidth =
|
newDisplay.originalWidth = int.tryParse(
|
||||||
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
|
evt['original_width'] ?? kInvalidResolutionValue.toString()) ??
|
||||||
newDisplay.originalHeight =
|
kInvalidResolutionValue;
|
||||||
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
|
newDisplay.originalHeight = int.tryParse(
|
||||||
|
evt['original_height'] ?? kInvalidResolutionValue.toString()) ??
|
||||||
|
kInvalidResolutionValue;
|
||||||
newDisplay._scale = _pi.scaleOfDisplay(display);
|
newDisplay._scale = _pi.scaleOfDisplay(display);
|
||||||
_pi.displays[display] = newDisplay;
|
_pi.displays[display] = newDisplay;
|
||||||
|
|
||||||
@@ -511,7 +531,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.target?.recordingModel.onSwitchDisplay();
|
|
||||||
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
|
if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) {
|
||||||
handleResolutions(peerId, evt['resolutions']);
|
handleResolutions(peerId, evt['resolutions']);
|
||||||
}
|
}
|
||||||
@@ -582,13 +601,44 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleToast(Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||||
|
final type = evt['type'] ?? 'info';
|
||||||
|
final text = evt['text'] ?? '';
|
||||||
|
final durMsc = evt['dur_msec'] ?? 2000;
|
||||||
|
final duration = Duration(milliseconds: durMsc);
|
||||||
|
if ((text).isEmpty) {
|
||||||
|
BotToast.showLoading(
|
||||||
|
duration: duration,
|
||||||
|
clickClose: true,
|
||||||
|
allowClick: true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (type.contains('error')) {
|
||||||
|
BotToast.showText(
|
||||||
|
contentColor: Colors.red,
|
||||||
|
text: translate(text),
|
||||||
|
duration: duration,
|
||||||
|
clickClose: true,
|
||||||
|
onlyOne: true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
BotToast.showText(
|
||||||
|
text: translate(text),
|
||||||
|
duration: duration,
|
||||||
|
clickClose: true,
|
||||||
|
onlyOne: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Show a message box with [type], [title] and [text].
|
/// Show a message box with [type], [title] and [text].
|
||||||
showMsgBox(SessionID sessionId, String type, String title, String text,
|
showMsgBox(SessionID sessionId, String type, String title, String text,
|
||||||
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
String link, bool hasRetry, OverlayDialogManager dialogManager,
|
||||||
{bool? hasCancel}) {
|
{bool? hasCancel}) {
|
||||||
msgBox(sessionId, type, title, text, link, dialogManager,
|
msgBox(sessionId, type, title, text, link, dialogManager,
|
||||||
hasCancel: hasCancel,
|
hasCancel: hasCancel,
|
||||||
reconnect: reconnect,
|
reconnect: hasRetry ? reconnect : null,
|
||||||
reconnectTimeout: hasRetry ? _reconnects : null);
|
reconnectTimeout: hasRetry ? _reconnects : null);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
if (hasRetry) {
|
if (hasRetry) {
|
||||||
@@ -788,7 +838,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
}
|
}
|
||||||
Map<String, dynamic> features = json.decode(evt['features']);
|
Map<String, dynamic> features = json.decode(evt['features']);
|
||||||
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
_pi.features.privacyMode = features['privacy_mode'] == true;
|
||||||
if (!isCache) {
|
if (!isCache) {
|
||||||
handleResolutions(peerId, evt["resolutions"]);
|
handleResolutions(peerId, evt["resolutions"]);
|
||||||
}
|
}
|
||||||
@@ -832,7 +882,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
for (final mode in [kKeyMapMode, kKeyLegacyMode]) {
|
for (final mode in [kKeyMapMode, kKeyLegacyMode]) {
|
||||||
if (bind.sessionIsKeyboardModeSupported(
|
if (bind.sessionIsKeyboardModeSupported(
|
||||||
sessionId: sessionId, mode: mode)) {
|
sessionId: sessionId, mode: mode)) {
|
||||||
bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
|
await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1088,8 +1138,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
// Directly switch to the new display without waiting for the response.
|
// Directly switch to the new display without waiting for the response.
|
||||||
switchToNewDisplay(int display, SessionID sessionId, String peerId,
|
switchToNewDisplay(int display, SessionID sessionId, String peerId,
|
||||||
{bool updateCursorPos = false}) {
|
{bool updateCursorPos = false}) {
|
||||||
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
|
|
||||||
parent.target?.recordingModel.onClose();
|
|
||||||
// no need to wait for the response
|
// no need to wait for the response
|
||||||
pi.currentDisplay = display;
|
pi.currentDisplay = display;
|
||||||
updateCurDisplay(sessionId, updateCursorPos: updateCursorPos);
|
updateCurDisplay(sessionId, updateCursorPos: updateCursorPos);
|
||||||
@@ -1178,6 +1226,27 @@ class ImageModel with ChangeNotifier {
|
|||||||
|
|
||||||
clearImage() => _image = null;
|
clearImage() => _image = null;
|
||||||
|
|
||||||
|
bool _webDecodingRgba = false;
|
||||||
|
final List<Uint8List> _webRgbaList = List.empty(growable: true);
|
||||||
|
webOnRgba(int display, Uint8List rgba) async {
|
||||||
|
// deep copy needed, otherwise "instantiateCodec failed: TypeError: Cannot perform Construct on a detached ArrayBuffer"
|
||||||
|
_webRgbaList.add(Uint8List.fromList(rgba));
|
||||||
|
if (_webDecodingRgba) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_webDecodingRgba = true;
|
||||||
|
try {
|
||||||
|
while (_webRgbaList.isNotEmpty) {
|
||||||
|
final rgba2 = _webRgbaList.last;
|
||||||
|
_webRgbaList.clear();
|
||||||
|
await decodeAndUpdate(display, rgba2);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('onRgba error: $e');
|
||||||
|
}
|
||||||
|
_webDecodingRgba = false;
|
||||||
|
}
|
||||||
|
|
||||||
onRgba(int display, Uint8List rgba) async {
|
onRgba(int display, Uint8List rgba) async {
|
||||||
try {
|
try {
|
||||||
await decodeAndUpdate(display, rgba);
|
await decodeAndUpdate(display, rgba);
|
||||||
@@ -1590,11 +1659,25 @@ class CanvasModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
clear([bool notify = false]) {
|
// For reset canvas to the last view style
|
||||||
|
reset() {
|
||||||
|
_scale = _lastViewStyle.scale;
|
||||||
|
_devicePixelRatio = ui.window.devicePixelRatio;
|
||||||
|
if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) {
|
||||||
|
_scale = 1.0 / _devicePixelRatio;
|
||||||
|
}
|
||||||
|
final displayWidth = getDisplayWidth();
|
||||||
|
final displayHeight = getDisplayHeight();
|
||||||
|
_x = (size.width - displayWidth * _scale) / 2;
|
||||||
|
_y = (size.height - displayHeight * _scale) / 2;
|
||||||
|
bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
_x = 0;
|
_x = 0;
|
||||||
_y = 0;
|
_y = 0;
|
||||||
_scale = 1.0;
|
_scale = 1.0;
|
||||||
if (notify) notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScrollPercent() {
|
updateScrollPercent() {
|
||||||
@@ -1919,7 +2002,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
_x = _displayOriginX;
|
_x = _displayOriginX;
|
||||||
_y = _displayOriginY;
|
_y = _displayOriginY;
|
||||||
parent.target?.inputModel.moveMouse(_x, _y);
|
parent.target?.inputModel.moveMouse(_x, _y);
|
||||||
parent.target?.canvasModel.clear(true);
|
parent.target?.canvasModel.reset();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2178,6 +2261,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
debugPrint("deleting cursor with key $k");
|
debugPrint("deleting cursor with key $k");
|
||||||
deleteCustomCursor(k);
|
deleteCustomCursor(k);
|
||||||
}
|
}
|
||||||
|
resetSystemCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
trySetRemoteWindowCoords() {
|
trySetRemoteWindowCoords() {
|
||||||
@@ -2224,8 +2308,10 @@ class QualityMonitorModel with ChangeNotifier {
|
|||||||
|
|
||||||
updateQualityStatus(Map<String, dynamic> evt) {
|
updateQualityStatus(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed'];
|
if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) {
|
||||||
if ((evt['fps'] 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 fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
|
||||||
final pi = parent.target?.ffiModel.pi;
|
final pi = parent.target?.ffiModel.pi;
|
||||||
if (pi != null) {
|
if (pi != null) {
|
||||||
@@ -2246,14 +2332,18 @@ class QualityMonitorModel with ChangeNotifier {
|
|||||||
_data.fps = null;
|
_data.fps = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay'];
|
if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) {
|
||||||
if ((evt['target_bitrate'] as String).isNotEmpty) {
|
_data.delay = evt['delay'];
|
||||||
|
}
|
||||||
|
if (evt.containsKey('target_bitrate') &&
|
||||||
|
(evt['target_bitrate'] as String).isNotEmpty) {
|
||||||
_data.targetBitrate = evt['target_bitrate'];
|
_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'];
|
_data.codecFormat = evt['codec_format'];
|
||||||
}
|
}
|
||||||
if ((evt['chroma'] as String).isNotEmpty) {
|
if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) {
|
||||||
_data.chroma = evt['chroma'];
|
_data.chroma = evt['chroma'];
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -2267,25 +2357,7 @@ class RecordingModel with ChangeNotifier {
|
|||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
RecordingModel(this.parent);
|
RecordingModel(this.parent);
|
||||||
bool _start = false;
|
bool _start = false;
|
||||||
get start => _start;
|
bool get start => _start;
|
||||||
|
|
||||||
onSwitchDisplay() {
|
|
||||||
if (isIOS || !_start) return;
|
|
||||||
final sessionId = parent.target?.sessionId;
|
|
||||||
int? width = parent.target?.canvasModel.getDisplayWidth();
|
|
||||||
int? height = parent.target?.canvasModel.getDisplayHeight();
|
|
||||||
if (sessionId == null || width == null || height == null) return;
|
|
||||||
final pi = parent.target?.ffiModel.pi;
|
|
||||||
if (pi == null) return;
|
|
||||||
final currentDisplay = pi.currentDisplay;
|
|
||||||
if (currentDisplay == kAllDisplayValue) return;
|
|
||||||
bind.sessionRecordScreen(
|
|
||||||
sessionId: sessionId,
|
|
||||||
start: true,
|
|
||||||
display: currentDisplay,
|
|
||||||
width: width,
|
|
||||||
height: height);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle() async {
|
toggle() async {
|
||||||
if (isIOS) return;
|
if (isIOS) return;
|
||||||
@@ -2293,48 +2365,16 @@ class RecordingModel with ChangeNotifier {
|
|||||||
if (sessionId == null) return;
|
if (sessionId == null) return;
|
||||||
final pi = parent.target?.ffiModel.pi;
|
final pi = parent.target?.ffiModel.pi;
|
||||||
if (pi == null) return;
|
if (pi == null) return;
|
||||||
final currentDisplay = pi.currentDisplay;
|
bool value = !_start;
|
||||||
if (currentDisplay == kAllDisplayValue) return;
|
if (value) {
|
||||||
_start = !_start;
|
await sessionRefreshVideo(sessionId, pi);
|
||||||
notifyListeners();
|
|
||||||
await _sendStatusMessage(sessionId, pi, _start);
|
|
||||||
if (_start) {
|
|
||||||
sessionRefreshVideo(sessionId, pi);
|
|
||||||
if (versionCmp(pi.version, '1.2.4') >= 0) {
|
|
||||||
// will not receive SwitchDisplay since 1.2.4
|
|
||||||
onSwitchDisplay();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bind.sessionRecordScreen(
|
|
||||||
sessionId: sessionId,
|
|
||||||
start: false,
|
|
||||||
display: currentDisplay,
|
|
||||||
width: 0,
|
|
||||||
height: 0);
|
|
||||||
}
|
}
|
||||||
|
await bind.sessionRecordScreen(sessionId: sessionId, start: value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose() async {
|
updateStatus(bool status) {
|
||||||
if (isIOS) return;
|
_start = status;
|
||||||
final sessionId = parent.target?.sessionId;
|
notifyListeners();
|
||||||
if (sessionId == null) return;
|
|
||||||
if (!_start) return;
|
|
||||||
_start = false;
|
|
||||||
final pi = parent.target?.ffiModel.pi;
|
|
||||||
if (pi == null) return;
|
|
||||||
final currentDisplay = pi.currentDisplay;
|
|
||||||
if (currentDisplay == kAllDisplayValue) return;
|
|
||||||
await _sendStatusMessage(sessionId, pi, false);
|
|
||||||
bind.sessionRecordScreen(
|
|
||||||
sessionId: sessionId,
|
|
||||||
start: false,
|
|
||||||
display: currentDisplay,
|
|
||||||
width: 0,
|
|
||||||
height: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async {
|
|
||||||
await bind.sessionRecordStatus(sessionId: sessionId, status: status);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2383,6 +2423,9 @@ class FFI {
|
|||||||
late final ElevationModel elevationModel; // session
|
late final ElevationModel elevationModel; // session
|
||||||
late final CmFileModel cmFileModel; // cm
|
late final CmFileModel cmFileModel; // cm
|
||||||
late final TextureModel textureModel; //session
|
late final TextureModel textureModel; //session
|
||||||
|
late final Peers recentPeersModel; // global
|
||||||
|
late final Peers favoritePeersModel; // global
|
||||||
|
late final Peers lanPeersModel; // global
|
||||||
|
|
||||||
FFI(SessionID? sId) {
|
FFI(SessionID? sId) {
|
||||||
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
|
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
|
||||||
@@ -2403,6 +2446,16 @@ class FFI {
|
|||||||
elevationModel = ElevationModel(WeakReference(this));
|
elevationModel = ElevationModel(WeakReference(this));
|
||||||
cmFileModel = CmFileModel(WeakReference(this));
|
cmFileModel = CmFileModel(WeakReference(this));
|
||||||
textureModel = TextureModel(WeakReference(this));
|
textureModel = TextureModel(WeakReference(this));
|
||||||
|
recentPeersModel = Peers(
|
||||||
|
name: PeersModelName.recent,
|
||||||
|
loadEvent: LoadEvent.recent,
|
||||||
|
getInitPeers: null);
|
||||||
|
favoritePeersModel = Peers(
|
||||||
|
name: PeersModelName.favorite,
|
||||||
|
loadEvent: LoadEvent.favorite,
|
||||||
|
getInitPeers: null);
|
||||||
|
lanPeersModel = Peers(
|
||||||
|
name: PeersModelName.lan, loadEvent: LoadEvent.lan, getInitPeers: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mobile reuse FFI
|
/// Mobile reuse FFI
|
||||||
@@ -2423,6 +2476,7 @@ class FFI {
|
|||||||
String? switchUuid,
|
String? switchUuid,
|
||||||
String? password,
|
String? password,
|
||||||
bool? isSharedPassword,
|
bool? isSharedPassword,
|
||||||
|
String? connToken,
|
||||||
bool? forceRelay,
|
bool? forceRelay,
|
||||||
int? tabWindowId,
|
int? tabWindowId,
|
||||||
int? display,
|
int? display,
|
||||||
@@ -2459,6 +2513,7 @@ class FFI {
|
|||||||
forceRelay: forceRelay ?? false,
|
forceRelay: forceRelay ?? false,
|
||||||
password: password ?? '',
|
password: password ?? '',
|
||||||
isSharedPassword: isSharedPassword ?? false,
|
isSharedPassword: isSharedPassword ?? false,
|
||||||
|
connToken: connToken,
|
||||||
);
|
);
|
||||||
} else if (display != null) {
|
} else if (display != null) {
|
||||||
if (displays == null) {
|
if (displays == null) {
|
||||||
@@ -2497,6 +2552,7 @@ class FFI {
|
|||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
imageModel.onRgba(display, data);
|
imageModel.onRgba(display, data);
|
||||||
});
|
});
|
||||||
|
this.id = id;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ class PlatformFFI {
|
|||||||
|
|
||||||
static get isMain => instance._appType == kAppTypeMain;
|
static get isMain => instance._appType == kAppTypeMain;
|
||||||
|
|
||||||
|
static String getByName(String name, [String arg = '']) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setByName(String name, [String value = '']) {}
|
||||||
|
|
||||||
static Future<String> getVersion() async {
|
static Future<String> getVersion() async {
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
return packageInfo.version;
|
return packageInfo.version;
|
||||||
@@ -276,4 +282,6 @@ class PlatformFFI {
|
|||||||
void syncAndroidServiceAppDirConfigPath() {
|
void syncAndroidServiceAppDirConfigPath() {
|
||||||
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setFullscreenCallback(void Function(bool) fun) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,10 +194,14 @@ class Peers extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateOnlineState(Map<String, dynamic> evt) {
|
void _updateOnlineState(Map<String, dynamic> evt) {
|
||||||
|
int changedCount = 0;
|
||||||
evt['onlines'].split(',').forEach((online) {
|
evt['onlines'].split(',').forEach((online) {
|
||||||
for (var i = 0; i < peers.length; i++) {
|
for (var i = 0; i < peers.length; i++) {
|
||||||
if (peers[i].id == online) {
|
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) {
|
evt['offlines'].split(',').forEach((offline) {
|
||||||
for (var i = 0; i < peers.length; i++) {
|
for (var i = 0; i < peers.length; i++) {
|
||||||
if (peers[i].id == offline) {
|
if (peers[i].id == offline) {
|
||||||
peers[i].online = false;
|
if (peers[i].online) {
|
||||||
|
changedCount += 1;
|
||||||
|
peers[i].online = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
event = UpdateEvent.online;
|
if (changedCount > 0) {
|
||||||
notifyListeners();
|
event = UpdateEvent.online;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updatePeers(Map<String, dynamic> evt) {
|
void _updatePeers(Map<String, dynamic> evt) {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
// https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700
|
// https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700
|
||||||
// After onTap, the shift key should be pressed for a while when not in multiselection mode,
|
// After onTap, the shift key should be pressed for a while when not in multiselection mode,
|
||||||
// because onTap is delayed when onDoubleTap is not null
|
// because onTap is delayed when onDoubleTap is not null
|
||||||
if (isDesktop && !_isShiftDown) return;
|
if (isDesktop || isWebDesktop) return;
|
||||||
_multiSelectionMode = true;
|
_multiSelectionMode = true;
|
||||||
}
|
}
|
||||||
final cached = _currentTabCachedPeers.map((e) => e.id).toList();
|
final cached = _currentTabCachedPeers.map((e) => e.id).toList();
|
||||||
@@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
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) {
|
setCurrentTabCachedPeers(List<Peer> peers) {
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
|
final isPreEmpty = _currentTabCachedPeers.isEmpty;
|
||||||
_currentTabCachedPeers = peers;
|
_currentTabCachedPeers = peers;
|
||||||
notifyListeners();
|
final isNowEmpty = _currentTabCachedPeers.isEmpty;
|
||||||
|
if (isPreEmpty != isNowEmpty) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,11 @@ final platformFFI = PlatformFFI.instance;
|
|||||||
final localeName = PlatformFFI.localeName;
|
final localeName = PlatformFFI.localeName;
|
||||||
|
|
||||||
RustdeskImpl get bind => platformFFI.ffiBind;
|
RustdeskImpl get bind => platformFFI.ffiBind;
|
||||||
|
|
||||||
|
String ffiGetByName(String name, [String arg = '']) {
|
||||||
|
return PlatformFFI.getByName(name, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ffiSetByName(String name, [String value = '']) {
|
||||||
|
PlatformFFI.setByName(name, value);
|
||||||
|
}
|
||||||
|
|||||||
@@ -826,7 +826,7 @@ class Client {
|
|||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final Map<String, dynamic> data = <String, dynamic>{};
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
data['id'] = id;
|
data['id'] = id;
|
||||||
data['is_start'] = authorized;
|
data['authorized'] = authorized;
|
||||||
data['is_file_transfer'] = isFileTransfer;
|
data['is_file_transfer'] = isFileTransfer;
|
||||||
data['port_forward'] = portForward;
|
data['port_forward'] = portForward;
|
||||||
data['name'] = name;
|
data['name'] = name;
|
||||||
@@ -840,6 +840,8 @@ class Client {
|
|||||||
data['block_input'] = blockInput;
|
data['block_input'] = blockInput;
|
||||||
data['disconnected'] = disconnected;
|
data['disconnected'] = disconnected;
|
||||||
data['from_switch'] = fromSwitch;
|
data['from_switch'] = fromSwitch;
|
||||||
|
data['in_voice_call'] = inVoiceCall;
|
||||||
|
data['incoming_voice_call'] = incomingVoiceCall;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class StateGlobal {
|
|||||||
final RxBool showRemoteToolBar = false.obs;
|
final RxBool showRemoteToolBar = false.obs;
|
||||||
final svcStatus = SvcStatus.notReady.obs;
|
final svcStatus = SvcStatus.notReady.obs;
|
||||||
final RxBool isFocused = false.obs;
|
final RxBool isFocused = false.obs;
|
||||||
|
// for mobile and web
|
||||||
|
bool isInMainPage = true;
|
||||||
|
bool isWebVisible = true;
|
||||||
|
|
||||||
|
final isPortrait = false.obs;
|
||||||
|
|
||||||
String _inputSource = '';
|
String _inputSource = '';
|
||||||
|
|
||||||
@@ -68,27 +73,40 @@ class StateGlobal {
|
|||||||
if (_fullscreen.value != v) {
|
if (_fullscreen.value != v) {
|
||||||
_fullscreen.value = v;
|
_fullscreen.value = v;
|
||||||
_showTabBar.value = !_fullscreen.value;
|
_showTabBar.value = !_fullscreen.value;
|
||||||
refreshResizeEdgeSize();
|
if (isWebDesktop) {
|
||||||
print(
|
procFullscreenWeb();
|
||||||
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
} else {
|
||||||
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
procFullscreenNative(procWnd);
|
||||||
if (procWnd) {
|
|
||||||
final wc = WindowController.fromWindowId(windowId);
|
|
||||||
wc.setFullscreen(_fullscreen.isTrue).then((_) {
|
|
||||||
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
|
|
||||||
if (isWindows && !v) {
|
|
||||||
Future.delayed(Duration.zero, () async {
|
|
||||||
final frame = await wc.getFrame();
|
|
||||||
final newRect = Rect.fromLTWH(
|
|
||||||
frame.left, frame.top, frame.width + 1, frame.height + 1);
|
|
||||||
await wc.setFrame(newRect);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
procFullscreenWeb() {
|
||||||
|
final isFullscreen = ffiGetByName('fullscreen') == 'Y';
|
||||||
|
String fullscreenValue = '';
|
||||||
|
if (isFullscreen && _fullscreen.isFalse) {
|
||||||
|
fullscreenValue = 'N';
|
||||||
|
} else if (!isFullscreen && fullscreen.isTrue) {
|
||||||
|
fullscreenValue = 'Y';
|
||||||
|
}
|
||||||
|
if (fullscreenValue.isNotEmpty) {
|
||||||
|
ffiSetByName('fullscreen', fullscreenValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
procFullscreenNative(bool procWnd) {
|
||||||
|
refreshResizeEdgeSize();
|
||||||
|
print("fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
||||||
|
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
||||||
|
if (procWnd) {
|
||||||
|
final wc = WindowController.fromWindowId(windowId);
|
||||||
|
wc.setFullscreen(_fullscreen.isTrue).then((_) {
|
||||||
|
// We remove the redraw (width + 1, height + 1), because this issue cannot be reproduced.
|
||||||
|
// https://github.com/rustdesk/rustdesk/issues/9675
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
|
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
|
||||||
? kFullScreenEdgeSize
|
? kFullScreenEdgeSize
|
||||||
: isMaximized.isTrue
|
: isMaximized.isTrue
|
||||||
@@ -107,7 +125,13 @@ class StateGlobal {
|
|||||||
_inputSource = bind.mainGetInputSource();
|
_inputSource = bind.mainGetInputSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
StateGlobal._();
|
StateGlobal._() {
|
||||||
|
if (isWebDesktop) {
|
||||||
|
platformFFI.setFullscreenCallback((v) {
|
||||||
|
_fullscreen.value = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static final StateGlobal instance = StateGlobal._();
|
static final StateGlobal instance = StateGlobal._();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:js_interop';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:js';
|
import 'dart:js';
|
||||||
import 'dart:html';
|
import 'dart:html';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/web/bridge.dart';
|
import 'package:flutter_hbb/web/bridge.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
@@ -28,7 +30,15 @@ class PlatformFFI {
|
|||||||
context.callMethod('setByName', [name, value]);
|
context.callMethod('setByName', [name, value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
PlatformFFI._();
|
PlatformFFI._() {
|
||||||
|
window.document.addEventListener(
|
||||||
|
'visibilitychange',
|
||||||
|
(event) => {
|
||||||
|
stateGlobal.isWebVisible =
|
||||||
|
window.document.visibilityState == 'visible'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static final PlatformFFI instance = PlatformFFI._();
|
static final PlatformFFI instance = PlatformFFI._();
|
||||||
|
|
||||||
static get localeName => window.navigator.language;
|
static get localeName => window.navigator.language;
|
||||||
@@ -98,6 +108,10 @@ class PlatformFFI {
|
|||||||
sessionId: sessionId, display: display, ptr: ptr);
|
sessionId: sessionId, display: display, ptr: ptr);
|
||||||
|
|
||||||
Future<void> init(String appType) async {
|
Future<void> init(String appType) async {
|
||||||
|
Completer completer = Completer();
|
||||||
|
context["onInitFinished"] = () {
|
||||||
|
completer.complete();
|
||||||
|
};
|
||||||
context.callMethod('init');
|
context.callMethod('init');
|
||||||
version = getByName('version');
|
version = getByName('version');
|
||||||
window.onContextMenu.listen((event) {
|
window.onContextMenu.listen((event) {
|
||||||
@@ -112,6 +126,7 @@ class PlatformFFI {
|
|||||||
print('json.decode fail(): $e');
|
print('json.decode fail(): $e');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setEventCallback(void Function(Map<String, dynamic>) fun) {
|
void setEventCallback(void Function(Map<String, dynamic>) fun) {
|
||||||
@@ -157,4 +172,10 @@ class PlatformFFI {
|
|||||||
|
|
||||||
// just for compilation
|
// just for compilation
|
||||||
void syncAndroidServiceAppDirConfigPath() {}
|
void syncAndroidServiceAppDirConfigPath() {}
|
||||||
|
|
||||||
|
void setFullscreenCallback(void Function(bool) fun) {
|
||||||
|
context["onFullscreenChanged"] = (bool v) {
|
||||||
|
fun(v);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,7 @@ final isWebDesktop_ = false;
|
|||||||
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||||
|
|
||||||
String get screenInfo_ => '';
|
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) =>
|
deleteCustomCursor(String key) =>
|
||||||
custom_cursor_manager.CursorManager.instance.deleteCursor(key);
|
custom_cursor_manager.CursorManager.instance.deleteCursor(key);
|
||||||
|
resetSystemCursor() {}
|
||||||
|
|
||||||
MouseCursor buildCursorOfCache(
|
MouseCursor buildCursorOfCache(
|
||||||
CursorModel cursor, double scale, CursorData? cache) {
|
CursorModel cursor, double scale, CursorData? cache) {
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ class RustDeskMultiWindowManager {
|
|||||||
bool withScreenRect,
|
bool withScreenRect,
|
||||||
) async {
|
) async {
|
||||||
final windowController = await DesktopMultiWindow.createWindow(msg);
|
final windowController = await DesktopMultiWindow.createWindow(msg);
|
||||||
|
if (isWindows) {
|
||||||
|
windowController.setInitBackgroundColor(Colors.black);
|
||||||
|
}
|
||||||
final windowId = windowController.windowId;
|
final windowId = windowController.windowId;
|
||||||
if (!withScreenRect) {
|
if (!withScreenRect) {
|
||||||
windowController
|
windowController
|
||||||
@@ -198,6 +201,7 @@ class RustDeskMultiWindowManager {
|
|||||||
String? switchUuid,
|
String? switchUuid,
|
||||||
bool? isRDP,
|
bool? isRDP,
|
||||||
bool? isSharedPassword,
|
bool? isSharedPassword,
|
||||||
|
String? connToken,
|
||||||
}) async {
|
}) async {
|
||||||
var params = {
|
var params = {
|
||||||
"type": type.index,
|
"type": type.index,
|
||||||
@@ -214,6 +218,9 @@ class RustDeskMultiWindowManager {
|
|||||||
if (isSharedPassword != null) {
|
if (isSharedPassword != null) {
|
||||||
params['isSharedPassword'] = isSharedPassword;
|
params['isSharedPassword'] = isSharedPassword;
|
||||||
}
|
}
|
||||||
|
if (connToken != null) {
|
||||||
|
params['connToken'] = connToken;
|
||||||
|
}
|
||||||
final msg = jsonEncode(params);
|
final msg = jsonEncode(params);
|
||||||
|
|
||||||
// separate window for file transfer is not supported
|
// separate window for file transfer is not supported
|
||||||
@@ -251,8 +258,13 @@ class RustDeskMultiWindowManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<MultiWindowCallResult> newFileTransfer(String remoteId,
|
Future<MultiWindowCallResult> newFileTransfer(
|
||||||
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
|
String remoteId, {
|
||||||
|
String? password,
|
||||||
|
bool? isSharedPassword,
|
||||||
|
bool? forceRelay,
|
||||||
|
String? connToken,
|
||||||
|
}) async {
|
||||||
return await newSession(
|
return await newSession(
|
||||||
WindowType.FileTransfer,
|
WindowType.FileTransfer,
|
||||||
kWindowEventNewFileTransfer,
|
kWindowEventNewFileTransfer,
|
||||||
@@ -261,11 +273,18 @@ class RustDeskMultiWindowManager {
|
|||||||
password: password,
|
password: password,
|
||||||
forceRelay: forceRelay,
|
forceRelay: forceRelay,
|
||||||
isSharedPassword: isSharedPassword,
|
isSharedPassword: isSharedPassword,
|
||||||
|
connToken: connToken,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
|
Future<MultiWindowCallResult> newPortForward(
|
||||||
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
|
String remoteId,
|
||||||
|
bool isRDP, {
|
||||||
|
String? password,
|
||||||
|
bool? isSharedPassword,
|
||||||
|
bool? forceRelay,
|
||||||
|
String? connToken,
|
||||||
|
}) async {
|
||||||
return await newSession(
|
return await newSession(
|
||||||
WindowType.PortForward,
|
WindowType.PortForward,
|
||||||
kWindowEventNewPortForward,
|
kWindowEventNewPortForward,
|
||||||
@@ -275,6 +294,7 @@ class RustDeskMultiWindowManager {
|
|||||||
forceRelay: forceRelay,
|
forceRelay: forceRelay,
|
||||||
isRDP: isRDP,
|
isRDP: isRDP,
|
||||||
isSharedPassword: isSharedPassword,
|
isSharedPassword: isSharedPassword,
|
||||||
|
connToken: connToken,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,7 @@
|
|||||||
import 'dart:js' as js;
|
import 'dart:js' as js;
|
||||||
|
import 'dart:html' as html;
|
||||||
|
// cycle imports, maybe we can improve this
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
|
||||||
final isAndroid_ = false;
|
final isAndroid_ = false;
|
||||||
final isIOS_ = false;
|
final isIOS_ = false;
|
||||||
@@ -11,3 +14,8 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
|
|||||||
final isDesktop_ = false;
|
final isDesktop_ = false;
|
||||||
|
|
||||||
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
|
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
|
||||||
|
|
||||||
|
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
|
||||||
|
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
|
||||||
|
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;
|
||||||
|
final isWebOnMacOS_ = _localOs == kPeerPlatformMacOS;
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ class CursorManager {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> resetSystemCursor() async {
|
||||||
|
latestKey = '';
|
||||||
|
js.context.callMethod('setByName', ['cursor', 'auto']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FlutterCustomMemoryImageCursor extends MouseCursor {
|
class FlutterCustomMemoryImageCursor extends MouseCursor {
|
||||||
@@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
|
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
|
||||||
|
resetSystemCursor() => CursorManager.instance.resetSystemCursor();
|
||||||
|
|
||||||
MouseCursor buildCursorOfCache(
|
MouseCursor buildCursorOfCache(
|
||||||
model.CursorModel cursor, double scale, model.CursorData? cache) {
|
model.CursorModel cursor, double scale, model.CursorData? cache) {
|
||||||
|
|||||||
14
flutter/lib/web/dummy.dart
Normal file
14
flutter/lib/web/dummy.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Future<void> webselectFiles({required bool is_folder}) async {
|
||||||
|
throw UnimplementedError("webselectFiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> webSendLocalFiles(
|
||||||
|
{required int handleIndex,
|
||||||
|
required int actId,
|
||||||
|
required String path,
|
||||||
|
required String to,
|
||||||
|
required int fileNum,
|
||||||
|
required bool includeHidden,
|
||||||
|
required bool isRemote}) {
|
||||||
|
throw UnimplementedError("webSendLocalFiles");
|
||||||
|
}
|
||||||
26
flutter/lib/web/settings_page.dart
Normal file
26
flutter/lib/web/settings_page.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||||
|
|
||||||
|
class WebSettingsPage extends StatelessWidget {
|
||||||
|
const WebSettingsPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _buildDesktopButton(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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ class TextureRgbaRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> closeTexture(int key) {
|
Future<bool> closeTexture(int key) {
|
||||||
throw UnimplementedError();
|
return Future(() => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> onRgba(
|
Future<bool> onRgba(
|
||||||
|
|||||||
30
flutter/lib/web/web_unique.dart
Normal file
30
flutter/lib/web/web_unique.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:js' as js;
|
||||||
|
|
||||||
|
Future<void> webselectFiles({required bool is_folder}) async {
|
||||||
|
return Future(
|
||||||
|
() => js.context.callMethod('setByName', ['select_files', is_folder]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> webSendLocalFiles(
|
||||||
|
{required int handleIndex,
|
||||||
|
required int actId,
|
||||||
|
required String path,
|
||||||
|
required String to,
|
||||||
|
required int fileNum,
|
||||||
|
required bool includeHidden,
|
||||||
|
required bool isRemote}) {
|
||||||
|
return Future(() => js.context.callMethod('setByName', [
|
||||||
|
'send_local_files',
|
||||||
|
jsonEncode({
|
||||||
|
'id': actId,
|
||||||
|
'handle_index': handleIndex,
|
||||||
|
'path': path,
|
||||||
|
'to': to,
|
||||||
|
'file_num': fileNum,
|
||||||
|
'include_hidden': includeHidden,
|
||||||
|
'is_remote': isRemote,
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
}
|
||||||
@@ -95,17 +95,17 @@ SPEC CHECKSUMS:
|
|||||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||||
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||||
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
|
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||||
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
|
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
|
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
|
||||||
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
|
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
|
||||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
||||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926"
|
resolved-ref: "519350f1f40746798299e94786197d058353bac9"
|
||||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@@ -380,6 +380,22 @@ packages:
|
|||||||
url: "https://github.com/rustdesk-org/dynamic_layouts.git"
|
url: "https://github.com/rustdesk-org/dynamic_layouts.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1+1"
|
version: "0.0.1+1"
|
||||||
|
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:
|
external_path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -509,8 +525,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
|
||||||
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
|
||||||
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -1613,5 +1629,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1"
|
version: "0.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.0 <4.0.0"
|
dart: ">=3.3.0 <4.0.0"
|
||||||
flutter: ">=3.16.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
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.3.0+46
|
version: 1.3.2+51
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
@@ -93,7 +93,7 @@ dependencies:
|
|||||||
flutter_gpu_texture_renderer:
|
flutter_gpu_texture_renderer:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
||||||
ref: 38951317afe79d953ab25733667bd96e172a80d3
|
ref: 2ded7f146437a761ffe6981e2f742038f85ca68d
|
||||||
uuid: ^3.0.7
|
uuid: ^3.0.7
|
||||||
auto_size_text_field: ^2.2.1
|
auto_size_text_field: ^2.2.1
|
||||||
flex_color_picker: ^3.3.0
|
flex_color_picker: ^3.3.0
|
||||||
@@ -104,6 +104,7 @@ dependencies:
|
|||||||
pull_down_button: ^0.9.3
|
pull_down_button: ^0.9.3
|
||||||
device_info_plus: ^9.1.0
|
device_info_plus: ^9.1.0
|
||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
|
extended_text: 13.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
icons_launcher: ^2.0.4
|
icons_launcher: ^2.0.4
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
#[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::{
|
use hbb_common::{
|
||||||
lazy_static,
|
lazy_static,
|
||||||
tokio::sync::{
|
tokio::sync::{
|
||||||
@@ -25,6 +25,8 @@ pub use context_send::*;
|
|||||||
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
|
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
|
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;
|
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!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
|
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",))]
|
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||||
#[inline]
|
#[inline]
|
||||||
fn send_data(conn_id: i32, data: ClipboardFile) {
|
fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
return send_data_to_channel(conn_id, data);
|
return send_data_to_channel(conn_id, data);
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[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",))]
|
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||||
#[inline]
|
#[inline]
|
||||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) {
|
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||||
// no need to handle result here
|
|
||||||
if let Some(msg_channel) = VEC_MSG_CHANNEL
|
if let Some(msg_channel) = VEC_MSG_CHANNEL
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|x| x.conn_id == conn_id)
|
.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")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
#[inline]
|
#[inline]
|
||||||
fn send_data_to_all(data: ClipboardFile) {
|
fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
|
||||||
// no need to handle result here
|
// Need more tests to see if it's necessary to handle the error.
|
||||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||||
allow_err!(msg_channel.sender.send(data.clone()));
|
allow_err!(msg_channel.sender.send(data.clone()));
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
||||||
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
||||||
};
|
};
|
||||||
use hbb_common::log;
|
use hbb_common::log;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -998,7 +998,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// no need to handle result here
|
// no need to handle result here
|
||||||
send_data(conn_id as _, data);
|
allow_err!(send_data(conn_id as _, data));
|
||||||
|
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -1045,7 +1045,13 @@ extern "C" fn client_format_list(
|
|||||||
.iter()
|
.iter()
|
||||||
.for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone())));
|
.for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone())));
|
||||||
} else {
|
} 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
|
0
|
||||||
@@ -1067,9 +1073,13 @@ extern "C" fn client_format_list_response(
|
|||||||
msg_flags
|
msg_flags
|
||||||
);
|
);
|
||||||
let data = ClipboardFile::FormatListResponse { msg_flags };
|
let data = ClipboardFile::FormatListResponse { msg_flags };
|
||||||
send_data(conn_id, data);
|
match send_data(conn_id, data) {
|
||||||
|
Ok(_) => 0,
|
||||||
0
|
Err(e) => {
|
||||||
|
log::error!("failed to send format list response: {:?}", e);
|
||||||
|
ERR_CODE_SEND_MSG
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn client_format_data_request(
|
extern "C" fn client_format_data_request(
|
||||||
@@ -1090,10 +1100,13 @@ extern "C" fn client_format_data_request(
|
|||||||
conn_id,
|
conn_id,
|
||||||
requested_format_id
|
requested_format_id
|
||||||
);
|
);
|
||||||
// no need to handle result here
|
match send_data(conn_id, data) {
|
||||||
send_data(conn_id, data);
|
Ok(_) => 0,
|
||||||
|
Err(e) => {
|
||||||
0
|
log::error!("failed to send format data request: {:?}", e);
|
||||||
|
ERR_CODE_SEND_MSG
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn client_format_data_response(
|
extern "C" fn client_format_data_response(
|
||||||
@@ -1125,9 +1138,13 @@ extern "C" fn client_format_data_response(
|
|||||||
msg_flags,
|
msg_flags,
|
||||||
format_data,
|
format_data,
|
||||||
};
|
};
|
||||||
send_data(conn_id, data);
|
match send_data(conn_id, data) {
|
||||||
|
Ok(_) => 0,
|
||||||
0
|
Err(e) => {
|
||||||
|
log::error!("failed to send format data response: {:?}", e);
|
||||||
|
ERR_CODE_SEND_MSG
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn client_file_contents_request(
|
extern "C" fn client_file_contents_request(
|
||||||
@@ -1175,9 +1192,13 @@ extern "C" fn client_file_contents_request(
|
|||||||
clip_data_id,
|
clip_data_id,
|
||||||
};
|
};
|
||||||
log::debug!("client_file_contents_request called, data: {:?}", &data);
|
log::debug!("client_file_contents_request called, data: {:?}", &data);
|
||||||
send_data(conn_id, data);
|
match send_data(conn_id, data) {
|
||||||
|
Ok(_) => 0,
|
||||||
0
|
Err(e) => {
|
||||||
|
log::error!("failed to send file contents request: {:?}", e);
|
||||||
|
ERR_CODE_SEND_MSG
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn client_file_contents_response(
|
extern "C" fn client_file_contents_response(
|
||||||
@@ -1213,7 +1234,11 @@ extern "C" fn client_file_contents_response(
|
|||||||
msg_flags,
|
msg_flags,
|
||||||
stream_id
|
stream_id
|
||||||
);
|
);
|
||||||
send_data(conn_id, data);
|
match send_data(conn_id, data) {
|
||||||
|
Ok(_) => 0,
|
||||||
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;
|
HWND hwnd;
|
||||||
HANDLE hmem;
|
HANDLE hmem;
|
||||||
HANDLE thread;
|
HANDLE thread;
|
||||||
HANDLE response_data_event;
|
HANDLE formatDataRespEvent;
|
||||||
|
BOOL formatDataRespReceived;
|
||||||
|
|
||||||
LPDATAOBJECT data_obj;
|
LPDATAOBJECT data_obj;
|
||||||
HANDLE data_obj_mutex;
|
HANDLE data_obj_mutex;
|
||||||
@@ -228,6 +229,7 @@ struct wf_clipboard
|
|||||||
ULONG req_fsize;
|
ULONG req_fsize;
|
||||||
char *req_fdata;
|
char *req_fdata;
|
||||||
HANDLE req_fevent;
|
HANDLE req_fevent;
|
||||||
|
BOOL req_f_received;
|
||||||
|
|
||||||
size_t nFiles;
|
size_t nFiles;
|
||||||
size_t file_array_size;
|
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,
|
static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid,
|
||||||
void **ppvObject)
|
void **ppvObject)
|
||||||
{
|
{
|
||||||
|
if (ppvObject == NULL)
|
||||||
|
return E_INVALIDARG;
|
||||||
|
|
||||||
if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown))
|
if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown))
|
||||||
{
|
{
|
||||||
IStream_AddRef(This);
|
IStream_AddRef(This);
|
||||||
@@ -362,6 +367,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
|
|||||||
}
|
}
|
||||||
|
|
||||||
*pcbRead = clipboard->req_fsize;
|
*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;
|
instance->m_lOffset.QuadPart += clipboard->req_fsize;
|
||||||
|
|
||||||
if (clipboard->req_fsize < cb)
|
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)
|
static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc)
|
||||||
{
|
{
|
||||||
IStream *iStream;
|
IStream *iStream = NULL;
|
||||||
BOOL success = FALSE;
|
BOOL success = FALSE;
|
||||||
BOOL isDir = FALSE;
|
BOOL isDir = FALSE;
|
||||||
CliprdrStream *instance;
|
CliprdrStream *instance = NULL;
|
||||||
wfClipboard *clipboard = (wfClipboard *)pData;
|
wfClipboard *clipboard = (wfClipboard *)pData;
|
||||||
|
|
||||||
|
if (!(pData && dsc))
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream));
|
instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream));
|
||||||
|
|
||||||
if (instance)
|
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,
|
static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count,
|
||||||
void *data)
|
void *data)
|
||||||
{
|
{
|
||||||
CliprdrDataObject *instance;
|
CliprdrDataObject *instance = NULL;
|
||||||
IDataObject *iDataObject;
|
IDataObject *iDataObject = NULL;
|
||||||
instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject));
|
instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject));
|
||||||
|
|
||||||
if (!instance)
|
if (!instance)
|
||||||
goto error;
|
goto error;
|
||||||
|
|
||||||
|
instance->m_pFormatEtc = NULL;
|
||||||
|
instance->m_pStgMedium = NULL;
|
||||||
|
|
||||||
iDataObject = &instance->iDataObject;
|
iDataObject = &instance->iDataObject;
|
||||||
|
iDataObject->lpVtbl = NULL;
|
||||||
iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl));
|
iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl));
|
||||||
|
|
||||||
if (!iDataObject->lpVtbl)
|
if (!iDataObject->lpVtbl)
|
||||||
@@ -929,7 +951,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc
|
|||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
error:
|
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;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,6 +1049,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT
|
|||||||
REFIID riid, void **ppvObject)
|
REFIID riid, void **ppvObject)
|
||||||
{
|
{
|
||||||
(void)This;
|
(void)This;
|
||||||
|
if (!ppvObject)
|
||||||
|
return E_INVALIDARG;
|
||||||
|
|
||||||
if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown))
|
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;
|
WCHAR *unicode_name;
|
||||||
#if !defined(UNICODE)
|
#if !defined(UNICODE)
|
||||||
size_t size;
|
size_t size;
|
||||||
|
int towchar_count;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!clipboard || !format_name)
|
if (!clipboard || !format_name)
|
||||||
@@ -1205,6 +1247,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
|
|||||||
|
|
||||||
#if defined(UNICODE)
|
#if defined(UNICODE)
|
||||||
unicode_name = _wcsdup(format_name);
|
unicode_name = _wcsdup(format_name);
|
||||||
|
if (!unicode_name)
|
||||||
|
return 0;
|
||||||
#else
|
#else
|
||||||
size = _tcslen(format_name);
|
size = _tcslen(format_name);
|
||||||
unicode_name = calloc(size + 1, sizeof(WCHAR));
|
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)
|
if (!unicode_name)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size);
|
towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0);
|
||||||
#endif
|
if (towchar_count <= 0 || towchar_count > size)
|
||||||
|
|
||||||
if (!unicode_name)
|
|
||||||
return 0;
|
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++)
|
for (i = 0; i < clipboard->map_size; i++)
|
||||||
{
|
{
|
||||||
@@ -1312,6 +1358,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
|||||||
if (!clipboard)
|
if (!clipboard)
|
||||||
return -1;
|
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)) ==
|
if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) ==
|
||||||
0)
|
0)
|
||||||
return -1;
|
return -1;
|
||||||
@@ -1444,7 +1493,37 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID)
|
|||||||
return rc;
|
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;
|
UINT rc = ERROR_SUCCESS;
|
||||||
clipboard->context->IsStopped = FALSE;
|
clipboard->context->IsStopped = FALSE;
|
||||||
@@ -1456,7 +1535,21 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
|
|||||||
DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis);
|
DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis);
|
||||||
if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE)
|
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)
|
if (clipboard->context->IsStopped == TRUE)
|
||||||
@@ -1470,12 +1563,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
|
|||||||
return ERROR_INTERNAL_ERROR;
|
return ERROR_INTERNAL_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ResetEvent(event))
|
|
||||||
{
|
|
||||||
// NOTE: critical error here, crash may be better
|
|
||||||
rc = ERROR_INTERNAL_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((*data) == NULL)
|
if ((*data) == NULL)
|
||||||
{
|
{
|
||||||
rc = ERROR_INTERNAL_ERROR;
|
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)
|
if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest)
|
||||||
return ERROR_INTERNAL_ERROR;
|
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);
|
remoteFormatId = get_remote_format_id(clipboard, formatId);
|
||||||
|
|
||||||
formatDataRequest.connID = connID;
|
formatDataRequest.connID = connID;
|
||||||
@@ -1530,7 +1624,7 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
|||||||
return rc;
|
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,
|
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)
|
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
||||||
return ERROR_INTERNAL_ERROR;
|
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;
|
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.streamId = (UINT32)(ULONG_PTR)streamid;
|
||||||
fileContentsRequest.listIndex = index;
|
fileContentsRequest.listIndex = index;
|
||||||
fileContentsRequest.dwFlags = flag;
|
fileContentsRequest.dwFlags = flag;
|
||||||
@@ -1558,7 +1662,7 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
|||||||
return rc;
|
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(
|
static UINT cliprdr_send_response_filecontents(
|
||||||
@@ -1788,6 +1892,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case WM_DESTROYCLIPBOARD:
|
case WM_DESTROYCLIPBOARD:
|
||||||
|
// to-do: clear clipboard data?
|
||||||
case WM_ASKCBFORMATNAME:
|
case WM_ASKCBFORMATNAME:
|
||||||
case WM_HSCROLLCLIPBOARD:
|
case WM_HSCROLLCLIPBOARD:
|
||||||
case WM_PAINTCLIPBOARD:
|
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)
|
LONG positionHigh, DWORD nRequested, DWORD *puSize)
|
||||||
{
|
{
|
||||||
BOOL res = FALSE;
|
BOOL res = FALSE;
|
||||||
HANDLE hFile;
|
HANDLE hFile = NULL;
|
||||||
DWORD nGet, rc;
|
DWORD nGet, rc;
|
||||||
|
|
||||||
if (!file_name || !buffer || !puSize)
|
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;
|
res = TRUE;
|
||||||
error:
|
error:
|
||||||
|
if (hFile)
|
||||||
if (!CloseHandle(hFile))
|
{
|
||||||
res = FALSE;
|
if (!CloseHandle(hFile))
|
||||||
|
res = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
if (res)
|
if (res)
|
||||||
*puSize = nGet;
|
*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 */
|
/* 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)
|
static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen)
|
||||||
{
|
{
|
||||||
HANDLE hFile;
|
HANDLE hFile = NULL;
|
||||||
FILEDESCRIPTORW *fd;
|
FILEDESCRIPTORW *fd = NULL;
|
||||||
fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW));
|
fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW));
|
||||||
|
|
||||||
if (!fd)
|
if (!fd)
|
||||||
@@ -1975,7 +2082,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh);
|
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);
|
CloseHandle(hFile);
|
||||||
|
|
||||||
return fd;
|
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])
|
if (!clipboard->file_names[clipboard->nFiles])
|
||||||
return FALSE;
|
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 */
|
/* add to descriptor array */
|
||||||
clipboard->fileDescriptor[clipboard->nFiles] =
|
clipboard->fileDescriptor[clipboard->nFiles] =
|
||||||
wf_cliprdr_get_file_descriptor(full_file_name, pathLen);
|
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)
|
if (!clipboard || !Dir)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
// StringCchCopy(DirSpec, MAX_PATH, Dir);
|
if (wcslen(Dir) + 3 > MAX_PATH)
|
||||||
// StringCchCat(DirSpec, MAX_PATH, TEXT("\\*"));
|
return FALSE;
|
||||||
StringCchCopyW(DirSpec, MAX_PATH, Dir);
|
StringCchCopyW(DirSpec, MAX_PATH, Dir);
|
||||||
StringCchCatW(DirSpec, MAX_PATH, L"\\*");
|
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)
|
if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
||||||
{
|
{
|
||||||
WCHAR DirAdd[MAX_PATH];
|
WCHAR DirAdd[MAX_PATH];
|
||||||
// StringCchCopy(DirAdd, MAX_PATH, Dir);
|
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
|
||||||
// StringCchCat(DirAdd, MAX_PATH, _T("\\"));
|
return FALSE;
|
||||||
// StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName);
|
|
||||||
StringCchCopyW(DirAdd, MAX_PATH, Dir);
|
StringCchCopyW(DirAdd, MAX_PATH, Dir);
|
||||||
StringCchCatW(DirAdd, MAX_PATH, L"\\");
|
StringCchCatW(DirAdd, MAX_PATH, L"\\");
|
||||||
StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName);
|
StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName);
|
||||||
@@ -2094,10 +2214,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
WCHAR fileName[MAX_PATH];
|
WCHAR fileName[MAX_PATH];
|
||||||
// StringCchCopy(fileName, MAX_PATH, Dir);
|
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
|
||||||
// StringCchCat(fileName, MAX_PATH, _T("\\"));
|
return FALSE;
|
||||||
// StringCchCat(fileName, MAX_PATH, FindFileData.cFileName);
|
|
||||||
|
|
||||||
StringCchCopyW(fileName, MAX_PATH, Dir);
|
StringCchCopyW(fileName, MAX_PATH, Dir);
|
||||||
StringCchCatW(fileName, MAX_PATH, L"\\");
|
StringCchCatW(fileName, MAX_PATH, L"\\");
|
||||||
StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName);
|
StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName);
|
||||||
@@ -2242,9 +2360,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
|||||||
if (context->EnableFiles)
|
if (context->EnableFiles)
|
||||||
{
|
{
|
||||||
UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32));
|
UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32));
|
||||||
*p_conn_id = formatList->connID;
|
if (p_conn_id) {
|
||||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
|
*p_conn_id = formatList->connID;
|
||||||
rc = CHANNEL_RC_OK;
|
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
|
||||||
|
rc = CHANNEL_RC_OK;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2265,16 +2385,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
|||||||
// SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL);
|
// SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL);
|
||||||
|
|
||||||
FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS));
|
FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS));
|
||||||
format_ids->connID = formatList->connID;
|
if (format_ids)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id;
|
format_ids->connID = formatList->connID;
|
||||||
}
|
format_ids->size = (UINT32)clipboard->map_size;
|
||||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids))
|
format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
|
||||||
{
|
if (format_ids->formats)
|
||||||
rc = CHANNEL_RC_OK;
|
{
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -2469,17 +2603,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
|
|||||||
p += len + 1, clipboard->nFiles++)
|
p += len + 1, clipboard->nFiles++)
|
||||||
{
|
{
|
||||||
int cchWideChar;
|
int cchWideChar;
|
||||||
WCHAR *wFileName;
|
|
||||||
cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0);
|
cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0);
|
||||||
wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR));
|
wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR));
|
||||||
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar);
|
if (wFileName)
|
||||||
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar);
|
{
|
||||||
|
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);
|
GlobalUnlock(stg_medium.hGlobal);
|
||||||
ReleaseStgMedium(&stg_medium);
|
ReleaseStgMedium(&stg_medium);
|
||||||
resp:
|
resp:
|
||||||
|
// size will not overflow, because size type is size_t (unsigned __int64)
|
||||||
size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW);
|
size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW);
|
||||||
groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size);
|
groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size);
|
||||||
|
|
||||||
@@ -2519,10 +2664,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
|
|||||||
globlemem = (char *)GlobalLock(hClipdata);
|
globlemem = (char *)GlobalLock(hClipdata);
|
||||||
size = (int)GlobalSize(hClipdata);
|
size = (int)GlobalSize(hClipdata);
|
||||||
buff = malloc(size);
|
buff = malloc(size);
|
||||||
CopyMemory(buff, globlemem, size);
|
if (buff)
|
||||||
|
{
|
||||||
|
CopyMemory(buff, globlemem, size);
|
||||||
|
rc = ERROR_SUCCESS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rc = ERROR_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
GlobalUnlock(hClipdata);
|
GlobalUnlock(hClipdata);
|
||||||
CloseClipboard();
|
CloseClipboard();
|
||||||
rc = ERROR_SUCCESS;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2545,7 +2697,7 @@ exit:
|
|||||||
response.requestedFormatData = (BYTE *)buff;
|
response.requestedFormatData = (BYTE *)buff;
|
||||||
if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response))
|
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)
|
if (buff)
|
||||||
@@ -2621,9 +2773,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
|
|||||||
rc = CHANNEL_RC_OK;
|
rc = CHANNEL_RC_OK;
|
||||||
} while (0);
|
} 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;
|
rc = ERROR_INTERNAL_ERROR;
|
||||||
}
|
}
|
||||||
return rc;
|
return rc;
|
||||||
@@ -2899,7 +3053,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
|||||||
|
|
||||||
if (!SetEvent(clipboard->req_fevent))
|
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;
|
return rc;
|
||||||
}
|
}
|
||||||
@@ -2934,14 +3090,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
|
|||||||
(formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping))))
|
(formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping))))
|
||||||
goto error;
|
goto error;
|
||||||
|
|
||||||
if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
||||||
goto error;
|
goto error;
|
||||||
|
clipboard->formatDataRespReceived = FALSE;
|
||||||
|
|
||||||
if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex")))
|
if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex")))
|
||||||
goto error;
|
goto error;
|
||||||
|
|
||||||
if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
||||||
goto error;
|
goto error;
|
||||||
|
clipboard->req_f_received = FALSE;
|
||||||
|
|
||||||
if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL)))
|
if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL)))
|
||||||
goto error;
|
goto error;
|
||||||
@@ -3002,8 +3160,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
|
|||||||
clipboard->data_obj = NULL;
|
clipboard->data_obj = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clipboard->response_data_event)
|
if (clipboard->formatDataRespEvent)
|
||||||
CloseHandle(clipboard->response_data_event);
|
CloseHandle(clipboard->formatDataRespEvent);
|
||||||
|
|
||||||
if (clipboard->data_obj_mutex)
|
if (clipboard->data_obj_mutex)
|
||||||
CloseHandle(clipboard->data_obj_mutex);
|
CloseHandle(clipboard->data_obj_mutex);
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3;
|
|||||||
const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31;
|
const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31;
|
||||||
const BUF_LEN: usize = 4;
|
const BUF_LEN: usize = 4;
|
||||||
|
|
||||||
|
const MOUSE_EVENT_BUTTON_NUMBER_BACK: i64 = 3;
|
||||||
|
const MOUSE_EVENT_BUTTON_NUMBER_FORWARD: i64 = 4;
|
||||||
|
|
||||||
/// The event source user data value of cgevent.
|
/// The event source user data value of cgevent.
|
||||||
pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100;
|
pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100;
|
||||||
|
|
||||||
@@ -108,11 +111,17 @@ pub struct Enigo {
|
|||||||
double_click_interval: u32,
|
double_click_interval: u32,
|
||||||
last_click_time: Option<std::time::Instant>,
|
last_click_time: Option<std::time::Instant>,
|
||||||
multiple_click: i64,
|
multiple_click: i64,
|
||||||
|
ignore_flags: bool,
|
||||||
flags: CGEventFlags,
|
flags: CGEventFlags,
|
||||||
char_to_vkey_map: Map<String, Map<char, CGKeyCode>>,
|
char_to_vkey_map: Map<String, Map<char, CGKeyCode>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Enigo {
|
impl Enigo {
|
||||||
|
/// Set if ignore flags when posting events.
|
||||||
|
pub fn set_ignore_flags(&mut self, ignore: bool) {
|
||||||
|
self.ignore_flags = ignore;
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
pub fn reset_flag(&mut self) {
|
pub fn reset_flag(&mut self) {
|
||||||
self.flags = CGEventFlags::CGEventFlagNull;
|
self.flags = CGEventFlags::CGEventFlagNull;
|
||||||
@@ -133,7 +142,9 @@ impl Enigo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn post(&self, event: CGEvent) {
|
fn post(&self, event: CGEvent) {
|
||||||
event.set_flags(self.flags);
|
if !self.ignore_flags {
|
||||||
|
event.set_flags(self.flags);
|
||||||
|
}
|
||||||
event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE);
|
event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE);
|
||||||
event.post(CGEventTapLocation::HID);
|
event.post(CGEventTapLocation::HID);
|
||||||
}
|
}
|
||||||
@@ -161,6 +172,7 @@ impl Default for Enigo {
|
|||||||
double_click_interval,
|
double_click_interval,
|
||||||
multiple_click: 1,
|
multiple_click: 1,
|
||||||
last_click_time: None,
|
last_click_time: None,
|
||||||
|
ignore_flags: false,
|
||||||
flags: CGEventFlags::CGEventFlagNull,
|
flags: CGEventFlags::CGEventFlagNull,
|
||||||
char_to_vkey_map: Default::default(),
|
char_to_vkey_map: Default::default(),
|
||||||
}
|
}
|
||||||
@@ -226,14 +238,24 @@ impl MouseControllable for Enigo {
|
|||||||
}
|
}
|
||||||
self.last_click_time = Some(now);
|
self.last_click_time = Some(now);
|
||||||
let (current_x, current_y) = Self::mouse_location();
|
let (current_x, current_y) = Self::mouse_location();
|
||||||
let (button, event_type) = match button {
|
let (button, event_type, btn_value) = match button {
|
||||||
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown),
|
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, None),
|
||||||
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown),
|
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, None),
|
||||||
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown),
|
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown, None),
|
||||||
|
MouseButton::Back => (
|
||||||
|
CGMouseButton::Left,
|
||||||
|
CGEventType::OtherMouseDown,
|
||||||
|
Some(MOUSE_EVENT_BUTTON_NUMBER_BACK),
|
||||||
|
),
|
||||||
|
MouseButton::Forward => (
|
||||||
|
CGMouseButton::Left,
|
||||||
|
CGEventType::OtherMouseDown,
|
||||||
|
Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD),
|
||||||
|
),
|
||||||
_ => {
|
_ => {
|
||||||
log::info!("Unsupported button {:?}", button);
|
log::info!("Unsupported button {:?}", button);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
let dest = CGPoint::new(current_x as f64, current_y as f64);
|
let dest = CGPoint::new(current_x as f64, current_y as f64);
|
||||||
if let Some(src) = self.event_source.as_ref() {
|
if let Some(src) = self.event_source.as_ref() {
|
||||||
@@ -244,6 +266,9 @@ impl MouseControllable for Enigo {
|
|||||||
self.multiple_click,
|
self.multiple_click,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(v) = btn_value {
|
||||||
|
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v);
|
||||||
|
}
|
||||||
self.post(event);
|
self.post(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,14 +277,24 @@ impl MouseControllable for Enigo {
|
|||||||
|
|
||||||
fn mouse_up(&mut self, button: MouseButton) {
|
fn mouse_up(&mut self, button: MouseButton) {
|
||||||
let (current_x, current_y) = Self::mouse_location();
|
let (current_x, current_y) = Self::mouse_location();
|
||||||
let (button, event_type) = match button {
|
let (button, event_type, btn_value) = match button {
|
||||||
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp),
|
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp, None),
|
||||||
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp),
|
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp, None),
|
||||||
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp),
|
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp, None),
|
||||||
|
MouseButton::Back => (
|
||||||
|
CGMouseButton::Left,
|
||||||
|
CGEventType::OtherMouseUp,
|
||||||
|
Some(MOUSE_EVENT_BUTTON_NUMBER_BACK),
|
||||||
|
),
|
||||||
|
MouseButton::Forward => (
|
||||||
|
CGMouseButton::Left,
|
||||||
|
CGEventType::OtherMouseUp,
|
||||||
|
Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD),
|
||||||
|
),
|
||||||
_ => {
|
_ => {
|
||||||
log::info!("Unsupported button {:?}", button);
|
log::info!("Unsupported button {:?}", button);
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
let dest = CGPoint::new(current_x as f64, current_y as f64);
|
let dest = CGPoint::new(current_x as f64, current_y as f64);
|
||||||
if let Some(src) = self.event_source.as_ref() {
|
if let Some(src) = self.event_source.as_ref() {
|
||||||
@@ -270,6 +305,9 @@ impl MouseControllable for Enigo {
|
|||||||
self.multiple_click,
|
self.multiple_click,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(v) = btn_value {
|
||||||
|
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v);
|
||||||
|
}
|
||||||
self.post(event);
|
self.post(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,7 +383,7 @@ impl KeyboardControllable for Enigo {
|
|||||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_sequence(&mut self, sequence: &str) {
|
fn key_sequence(&mut self, sequence: &str) {
|
||||||
// NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68
|
// NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68
|
||||||
// TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time
|
// TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time
|
||||||
@@ -382,12 +420,10 @@ impl KeyboardControllable for Enigo {
|
|||||||
fn key_down(&mut self, key: Key) -> crate::ResultType {
|
fn key_down(&mut self, key: Key) -> crate::ResultType {
|
||||||
let code = self.key_to_keycode(key);
|
let code = self.key_to_keycode(key);
|
||||||
if code == u16::MAX {
|
if code == u16::MAX {
|
||||||
return Err("".into());
|
return Err("".into());
|
||||||
}
|
}
|
||||||
if let Some(src) = self.event_source.as_ref() {
|
if let Some(src) = self.event_source.as_ref() {
|
||||||
if let Ok(event) =
|
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) {
|
||||||
CGEvent::new_keyboard_event(src.clone(), code, true)
|
|
||||||
{
|
|
||||||
self.post(event);
|
self.post(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ enum ClipboardFormat {
|
|||||||
ImageRgba = 21;
|
ImageRgba = 21;
|
||||||
ImagePng = 22;
|
ImagePng = 22;
|
||||||
ImageSvg = 23;
|
ImageSvg = 23;
|
||||||
|
Special = 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Clipboard {
|
message Clipboard {
|
||||||
@@ -334,6 +335,8 @@ message Clipboard {
|
|||||||
int32 width = 3;
|
int32 width = 3;
|
||||||
int32 height = 4;
|
int32 height = 4;
|
||||||
ClipboardFormat format = 5;
|
ClipboardFormat format = 5;
|
||||||
|
// Special format name, only used when format is Special.
|
||||||
|
string special_name = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message MultiClipboards { repeated Clipboard clipboards = 1; }
|
message MultiClipboards { repeated Clipboard clipboards = 1; }
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub const REG_INTERVAL: i64 = 15_000;
|
|||||||
pub const COMPRESS_LEVEL: i32 = 3;
|
pub const COMPRESS_LEVEL: i32 = 3;
|
||||||
const SERIAL: i32 = 3;
|
const SERIAL: i32 = 3;
|
||||||
const PASSWORD_ENC_VERSION: &str = "00";
|
const PASSWORD_ENC_VERSION: &str = "00";
|
||||||
const ENCRYPT_MAX_LEN: usize = 128;
|
pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
@@ -296,6 +296,8 @@ pub struct PeerConfig {
|
|||||||
pub keyboard_mode: String,
|
pub keyboard_mode: String,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub view_only: ViewOnly,
|
pub view_only: ViewOnly,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub sync_init_clipboard: SyncInitClipboard,
|
||||||
// Mouse wheel or touchpad scroll mode
|
// Mouse wheel or touchpad scroll mode
|
||||||
#[serde(
|
#[serde(
|
||||||
default = "PeerConfig::default_reverse_mouse_wheel",
|
default = "PeerConfig::default_reverse_mouse_wheel",
|
||||||
@@ -373,6 +375,7 @@ impl Default for PeerConfig {
|
|||||||
ui_flutter: Default::default(),
|
ui_flutter: Default::default(),
|
||||||
info: Default::default(),
|
info: Default::default(),
|
||||||
transfer: Default::default(),
|
transfer: Default::default(),
|
||||||
|
sync_init_clipboard: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -962,6 +965,10 @@ impl Config {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_bool_option(k: &str) -> bool {
|
||||||
|
option2bool(k, &Self::get_option(k))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_option(k: String, v: String) {
|
pub fn set_option(k: String, v: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) {
|
if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) {
|
||||||
return;
|
return;
|
||||||
@@ -1462,6 +1469,13 @@ serde_field_bool!(
|
|||||||
"ViewOnly::default_view_only"
|
"ViewOnly::default_view_only"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
serde_field_bool!(
|
||||||
|
SyncInitClipboard,
|
||||||
|
"sync-init-clipboard",
|
||||||
|
default_sync_init_clipboard,
|
||||||
|
"SyncInitClipboard::default_sync_init_clipboard"
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct LocalConfig {
|
pub struct LocalConfig {
|
||||||
#[serde(default, deserialize_with = "deserialize_string")]
|
#[serde(default, deserialize_with = "deserialize_string")]
|
||||||
@@ -1548,6 +1562,21 @@ impl LocalConfig {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Usually get_option should be used.
|
||||||
|
pub fn get_option_from_file(k: &str) -> String {
|
||||||
|
get_or(
|
||||||
|
&OVERWRITE_LOCAL_SETTINGS,
|
||||||
|
&Self::load().options,
|
||||||
|
&DEFAULT_LOCAL_SETTINGS,
|
||||||
|
k,
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bool_option(k: &str) -> bool {
|
||||||
|
option2bool(k, &Self::get_option(k))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_option(k: String, v: String) {
|
pub fn set_option(k: String, v: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) {
|
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) {
|
||||||
return;
|
return;
|
||||||
@@ -2156,6 +2185,7 @@ pub mod keys {
|
|||||||
pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality";
|
pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality";
|
||||||
pub const OPTION_CUSTOM_FPS: &str = "custom-fps";
|
pub const OPTION_CUSTOM_FPS: &str = "custom-fps";
|
||||||
pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference";
|
pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference";
|
||||||
|
pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard";
|
||||||
pub const OPTION_THEME: &str = "theme";
|
pub const OPTION_THEME: &str = "theme";
|
||||||
pub const OPTION_LANGUAGE: &str = "lang";
|
pub const OPTION_LANGUAGE: &str = "lang";
|
||||||
pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left";
|
pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left";
|
||||||
@@ -2187,6 +2217,7 @@ pub mod keys {
|
|||||||
pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout";
|
pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout";
|
||||||
pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open";
|
pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open";
|
||||||
pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming";
|
pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming";
|
||||||
|
pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing";
|
||||||
pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory";
|
pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory";
|
||||||
pub const OPTION_ENABLE_ABR: &str = "enable-abr";
|
pub const OPTION_ENABLE_ABR: &str = "enable-abr";
|
||||||
pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper";
|
pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper";
|
||||||
@@ -2218,6 +2249,9 @@ pub mod keys {
|
|||||||
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
|
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
|
||||||
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
|
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
|
||||||
pub const OPTION_HIDE_TRAY: &str = "hide-tray";
|
pub const OPTION_HIDE_TRAY: &str = "hide-tray";
|
||||||
|
pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection";
|
||||||
|
pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password";
|
||||||
|
pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer";
|
||||||
|
|
||||||
// flutter local options
|
// flutter local options
|
||||||
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
||||||
@@ -2276,6 +2310,7 @@ pub mod keys {
|
|||||||
OPTION_CUSTOM_IMAGE_QUALITY,
|
OPTION_CUSTOM_IMAGE_QUALITY,
|
||||||
OPTION_CUSTOM_FPS,
|
OPTION_CUSTOM_FPS,
|
||||||
OPTION_CODEC_PREFERENCE,
|
OPTION_CODEC_PREFERENCE,
|
||||||
|
OPTION_SYNC_INIT_CLIPBOARD,
|
||||||
];
|
];
|
||||||
// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS
|
// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS
|
||||||
pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
|
pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
|
||||||
@@ -2306,6 +2341,8 @@ pub mod keys {
|
|||||||
OPTION_DISABLE_GROUP_PANEL,
|
OPTION_DISABLE_GROUP_PANEL,
|
||||||
OPTION_PRE_ELEVATE_SERVICE,
|
OPTION_PRE_ELEVATE_SERVICE,
|
||||||
OPTION_ALLOW_REMOTE_CM_MODIFICATION,
|
OPTION_ALLOW_REMOTE_CM_MODIFICATION,
|
||||||
|
OPTION_ALLOW_AUTO_RECORD_OUTGOING,
|
||||||
|
OPTION_VIDEO_SAVE_DIRECTORY,
|
||||||
];
|
];
|
||||||
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
||||||
pub const KEYS_SETTINGS: &[&str] = &[
|
pub const KEYS_SETTINGS: &[&str] = &[
|
||||||
@@ -2327,7 +2364,6 @@ pub mod keys {
|
|||||||
OPTION_AUTO_DISCONNECT_TIMEOUT,
|
OPTION_AUTO_DISCONNECT_TIMEOUT,
|
||||||
OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN,
|
OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN,
|
||||||
OPTION_ALLOW_AUTO_RECORD_INCOMING,
|
OPTION_ALLOW_AUTO_RECORD_INCOMING,
|
||||||
OPTION_VIDEO_SAVE_DIRECTORY,
|
|
||||||
OPTION_ENABLE_ABR,
|
OPTION_ENABLE_ABR,
|
||||||
OPTION_ALLOW_REMOVE_WALLPAPER,
|
OPTION_ALLOW_REMOVE_WALLPAPER,
|
||||||
OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER,
|
OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER,
|
||||||
@@ -2362,6 +2398,9 @@ pub mod keys {
|
|||||||
OPTION_HIDE_HELP_CARDS,
|
OPTION_HIDE_HELP_CARDS,
|
||||||
OPTION_DEFAULT_CONNECT_PASSWORD,
|
OPTION_DEFAULT_CONNECT_PASSWORD,
|
||||||
OPTION_HIDE_TRAY,
|
OPTION_HIDE_TRAY,
|
||||||
|
OPTION_ONE_WAY_CLIPBOARD_REDIRECTION,
|
||||||
|
OPTION_ALLOW_LOGON_SCREEN_PASSWORD,
|
||||||
|
OPTION_ONE_WAY_FILE_TRANSFER,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String
|
|||||||
log::error!("Duplicate encryption!");
|
log::error!("Duplicate encryption!");
|
||||||
return s.to_owned();
|
return s.to_owned();
|
||||||
}
|
}
|
||||||
if s.bytes().len() > max_len {
|
if s.chars().count() > max_len {
|
||||||
return String::default();
|
return String::default();
|
||||||
}
|
}
|
||||||
if version == "00" {
|
if version == "00" {
|
||||||
if let Ok(s) = encrypt(s.as_bytes(), max_len) {
|
if let Ok(s) = encrypt(s.as_bytes()) {
|
||||||
return version.to_owned() + &s;
|
return version.to_owned() + &s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec<u
|
|||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
if version == "00" {
|
if version == "00" {
|
||||||
if let Ok(s) = encrypt(v, max_len) {
|
if let Ok(s) = encrypt(v) {
|
||||||
let mut version = version.to_owned().into_bytes();
|
let mut version = version.to_owned().into_bytes();
|
||||||
version.append(&mut s.into_bytes());
|
version.append(&mut s.into_bytes());
|
||||||
return version;
|
return version;
|
||||||
@@ -155,8 +155,8 @@ pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, boo
|
|||||||
(v.to_owned(), false, !v.is_empty())
|
(v.to_owned(), false, !v.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encrypt(v: &[u8], max_len: usize) -> Result<String, ()> {
|
fn encrypt(v: &[u8]) -> Result<String, ()> {
|
||||||
if !v.is_empty() && v.len() <= max_len {
|
if !v.is_empty() {
|
||||||
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
|
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
Err(())
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
|
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
|
||||||
pub const DISPLAY_SERVER_X11: &str = "x11";
|
pub const DISPLAY_SERVER_X11: &str = "x11";
|
||||||
|
pub const DISPLAY_DESKTOP_KDE: &str = "KDE";
|
||||||
|
|
||||||
|
pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP";
|
||||||
|
|
||||||
pub struct Distro {
|
pub struct Distro {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -29,6 +32,15 @@ impl Distro {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_kde() -> bool {
|
||||||
|
if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) {
|
||||||
|
env == DISPLAY_DESKTOP_KDE
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_gdm_user(username: &str) -> bool {
|
pub fn is_gdm_user(username: &str) -> bool {
|
||||||
username == "gdm"
|
username == "gdm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.3.0"
|
version = "1.3.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
|
|||||||
@@ -62,4 +62,3 @@ gstreamer-video = { version = "0.16", optional = true }
|
|||||||
git = "https://github.com/rustdesk-org/hwcodec"
|
git = "https://github.com/rustdesk-org/hwcodec"
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::{
|
|||||||
aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
|
aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
|
||||||
common::GoogleImage,
|
common::GoogleImage,
|
||||||
vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
|
vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
|
||||||
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb,
|
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture,
|
||||||
};
|
};
|
||||||
|
|
||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
@@ -623,7 +623,7 @@ impl Decoder {
|
|||||||
&mut self,
|
&mut self,
|
||||||
frame: &video_frame::Union,
|
frame: &video_frame::Union,
|
||||||
rgb: &mut ImageRgb,
|
rgb: &mut ImageRgb,
|
||||||
_texture: &mut *mut c_void,
|
_texture: &mut ImageTexture,
|
||||||
_pixelbuffer: &mut bool,
|
_pixelbuffer: &mut bool,
|
||||||
chroma: &mut Option<Chroma>,
|
chroma: &mut Option<Chroma>,
|
||||||
) -> ResultType<bool> {
|
) -> ResultType<bool> {
|
||||||
@@ -777,12 +777,16 @@ impl Decoder {
|
|||||||
fn handle_vram_video_frame(
|
fn handle_vram_video_frame(
|
||||||
decoder: &mut VRamDecoder,
|
decoder: &mut VRamDecoder,
|
||||||
frames: &EncodedVideoFrames,
|
frames: &EncodedVideoFrames,
|
||||||
texture: &mut *mut c_void,
|
texture: &mut ImageTexture,
|
||||||
) -> ResultType<bool> {
|
) -> ResultType<bool> {
|
||||||
let mut ret = false;
|
let mut ret = false;
|
||||||
for h26x in frames.frames.iter() {
|
for h26x in frames.frames.iter() {
|
||||||
for image in decoder.decode(&h26x.data)? {
|
for image in decoder.decode(&h26x.data)? {
|
||||||
*texture = image.frame.texture;
|
*texture = ImageTexture {
|
||||||
|
texture: image.frame.texture,
|
||||||
|
w: image.frame.width as _,
|
||||||
|
h: image.frame.height as _,
|
||||||
|
};
|
||||||
ret = true;
|
ret = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,6 +498,15 @@ pub struct HwCodecConfig {
|
|||||||
pub vram_decode: Vec<hwcodec::vram::DecodeContext>,
|
pub vram_decode: Vec<hwcodec::vram::DecodeContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HwCodecConfig2 is used to store the config in json format,
|
||||||
|
// confy can't serde HwCodecConfig successfully if the non-first struct Vec is empty due to old toml version.
|
||||||
|
// struct T { a: Vec<A>, b: Vec<String>} will fail if b is empty, but struct T { a: Vec<String>, b: Vec<String>} is ok.
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
struct HwCodecConfig2 {
|
||||||
|
#[serde(default)]
|
||||||
|
pub config: String,
|
||||||
|
}
|
||||||
|
|
||||||
// ipc server process start check process once, other process get from ipc server once
|
// ipc server process start check process once, other process get from ipc server once
|
||||||
// install: --server start check process, check process send to --server, ui get from --server
|
// install: --server start check process, check process send to --server, ui get from --server
|
||||||
// portable: ui start check process, check process send to ui
|
// portable: ui start check process, check process send to ui
|
||||||
@@ -509,7 +518,12 @@ impl HwCodecConfig {
|
|||||||
log::info!("set hwcodec config");
|
log::info!("set hwcodec config");
|
||||||
log::debug!("{config:?}");
|
log::debug!("{config:?}");
|
||||||
#[cfg(any(windows, target_os = "macos"))]
|
#[cfg(any(windows, target_os = "macos"))]
|
||||||
hbb_common::config::common_store(&config, "_hwcodec");
|
hbb_common::config::common_store(
|
||||||
|
&HwCodecConfig2 {
|
||||||
|
config: serde_json::to_string_pretty(&config).unwrap_or_default(),
|
||||||
|
},
|
||||||
|
"_hwcodec",
|
||||||
|
);
|
||||||
*CONFIG.lock().unwrap() = Some(config);
|
*CONFIG.lock().unwrap() = Some(config);
|
||||||
*CONFIG_SET_BY_IPC.lock().unwrap() = true;
|
*CONFIG_SET_BY_IPC.lock().unwrap() = true;
|
||||||
}
|
}
|
||||||
@@ -587,7 +601,8 @@ impl HwCodecConfig {
|
|||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
None => {
|
None => {
|
||||||
log::info!("try load cached hwcodec config");
|
log::info!("try load cached hwcodec config");
|
||||||
let c = hbb_common::config::common_load::<HwCodecConfig>("_hwcodec");
|
let c = hbb_common::config::common_load::<HwCodecConfig2>("_hwcodec");
|
||||||
|
let c: HwCodecConfig = serde_json::from_str(&c.config).unwrap_or_default();
|
||||||
let new_signature = hwcodec::common::get_gpu_signature();
|
let new_signature = hwcodec::common::get_gpu_signature();
|
||||||
if c.signature == new_signature {
|
if c.signature == new_signature {
|
||||||
log::debug!("load cached hwcodec config: {c:?}");
|
log::debug!("load cached hwcodec config: {c:?}");
|
||||||
|
|||||||
@@ -96,6 +96,22 @@ impl ImageRgb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ImageTexture {
|
||||||
|
pub texture: *mut c_void,
|
||||||
|
pub w: usize,
|
||||||
|
pub h: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ImageTexture {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
texture: std::ptr::null_mut(),
|
||||||
|
w: 0,
|
||||||
|
h: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()> {
|
pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()> {
|
||||||
// does this really help?
|
// does this really help?
|
||||||
@@ -156,7 +172,7 @@ pub trait TraitPixelBuffer {
|
|||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "ios")))]
|
||||||
pub enum Frame<'a> {
|
pub enum Frame<'a> {
|
||||||
PixelBuffer(PixelBuffer<'a>),
|
PixelBuffer(PixelBuffer<'a>),
|
||||||
Texture(*mut c_void),
|
Texture((*mut c_void, usize)),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "ios")))]
|
||||||
@@ -164,7 +180,7 @@ impl Frame<'_> {
|
|||||||
pub fn valid<'a>(&'a self) -> bool {
|
pub fn valid<'a>(&'a self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(),
|
Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(),
|
||||||
Frame::Texture(texture) => !texture.is_null(),
|
Frame::Texture((texture, _)) => !texture.is_null(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +202,7 @@ impl Frame<'_> {
|
|||||||
|
|
||||||
pub enum EncodeInput<'a> {
|
pub enum EncodeInput<'a> {
|
||||||
YUV(&'a [u8]),
|
YUV(&'a [u8]),
|
||||||
Texture(*mut c_void),
|
Texture((*mut c_void, usize)),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> EncodeInput<'a> {
|
impl<'a> EncodeInput<'a> {
|
||||||
@@ -197,7 +213,7 @@ impl<'a> EncodeInput<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn texture(&self) -> ResultType<*mut c_void> {
|
pub fn texture(&self) -> ResultType<(*mut c_void, usize)> {
|
||||||
match self {
|
match self {
|
||||||
Self::Texture(f) => Ok(*f),
|
Self::Texture(f) => Ok(*f),
|
||||||
_ => bail!("not texture frame"),
|
_ => bail!("not texture frame"),
|
||||||
@@ -296,6 +312,19 @@ impl From<&VideoFrame> for CodecFormat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&video_frame::Union> for CodecFormat {
|
||||||
|
fn from(it: &video_frame::Union) -> Self {
|
||||||
|
match it {
|
||||||
|
video_frame::Union::Vp8s(_) => CodecFormat::VP8,
|
||||||
|
video_frame::Union::Vp9s(_) => CodecFormat::VP9,
|
||||||
|
video_frame::Union::Av1s(_) => CodecFormat::AV1,
|
||||||
|
video_frame::Union::H264s(_) => CodecFormat::H264,
|
||||||
|
video_frame::Union::H265s(_) => CodecFormat::H265,
|
||||||
|
_ => CodecFormat::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&CodecName> for CodecFormat {
|
impl From<&CodecName> for CodecFormat {
|
||||||
fn from(value: &CodecName) -> Self {
|
fn from(value: &CodecName) -> Self {
|
||||||
match value {
|
match value {
|
||||||
@@ -316,7 +345,7 @@ impl ToString for CodecFormat {
|
|||||||
CodecFormat::AV1 => "AV1".into(),
|
CodecFormat::AV1 => "AV1".into(),
|
||||||
CodecFormat::H264 => "H264".into(),
|
CodecFormat::H264 => "H264".into(),
|
||||||
CodecFormat::H265 => "H265".into(),
|
CodecFormat::H265 => "H265".into(),
|
||||||
CodecFormat::Unknown => "Unknow".into(),
|
CodecFormat::Unknown => "Unknown".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,22 +25,28 @@ pub struct RecorderContext {
|
|||||||
pub server: bool,
|
pub server: bool,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub dir: String,
|
pub dir: String,
|
||||||
|
pub display: usize,
|
||||||
|
pub tx: Option<Sender<RecordState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RecorderContext2 {
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
pub format: CodecFormat,
|
pub format: CodecFormat,
|
||||||
pub tx: Option<Sender<RecordState>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecorderContext {
|
impl RecorderContext2 {
|
||||||
pub fn set_filename(&mut self) -> ResultType<()> {
|
pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> {
|
||||||
if !PathBuf::from(&self.dir).exists() {
|
if !PathBuf::from(&ctx.dir).exists() {
|
||||||
std::fs::create_dir_all(&self.dir)?;
|
std::fs::create_dir_all(&ctx.dir)?;
|
||||||
}
|
}
|
||||||
let file = if self.server { "incoming" } else { "outgoing" }.to_string()
|
let file = if ctx.server { "incoming" } else { "outgoing" }.to_string()
|
||||||
+ "_"
|
+ "_"
|
||||||
+ &self.id.clone()
|
+ &ctx.id.clone()
|
||||||
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
|
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
|
||||||
|
+ &format!("display{}_", ctx.display)
|
||||||
+ &self.format.to_string().to_lowercase()
|
+ &self.format.to_string().to_lowercase()
|
||||||
+ if self.format == CodecFormat::VP9
|
+ if self.format == CodecFormat::VP9
|
||||||
|| self.format == CodecFormat::VP8
|
|| self.format == CodecFormat::VP8
|
||||||
@@ -50,11 +56,10 @@ impl RecorderContext {
|
|||||||
} else {
|
} else {
|
||||||
".mp4"
|
".mp4"
|
||||||
};
|
};
|
||||||
self.filename = PathBuf::from(&self.dir)
|
self.filename = PathBuf::from(&ctx.dir)
|
||||||
.join(file)
|
.join(file)
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
log::info!("video will save to {}", self.filename);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +68,7 @@ unsafe impl Send for Recorder {}
|
|||||||
unsafe impl Sync for Recorder {}
|
unsafe impl Sync for Recorder {}
|
||||||
|
|
||||||
pub trait RecorderApi {
|
pub trait RecorderApi {
|
||||||
fn new(ctx: RecorderContext) -> ResultType<Self>
|
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool;
|
fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool;
|
||||||
@@ -78,13 +83,15 @@ pub enum RecordState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Recorder {
|
pub struct Recorder {
|
||||||
pub inner: Box<dyn RecorderApi>,
|
pub inner: Option<Box<dyn RecorderApi>>,
|
||||||
ctx: RecorderContext,
|
ctx: RecorderContext,
|
||||||
|
ctx2: Option<RecorderContext2>,
|
||||||
pts: Option<i64>,
|
pts: Option<i64>,
|
||||||
|
check_failed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Recorder {
|
impl Deref for Recorder {
|
||||||
type Target = Box<dyn RecorderApi>;
|
type Target = Option<Box<dyn RecorderApi>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.inner
|
&self.inner
|
||||||
@@ -98,114 +105,123 @@ impl DerefMut for Recorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Recorder {
|
impl Recorder {
|
||||||
pub fn new(mut ctx: RecorderContext) -> ResultType<Self> {
|
pub fn new(ctx: RecorderContext) -> ResultType<Self> {
|
||||||
ctx.set_filename()?;
|
Ok(Self {
|
||||||
let recorder = match ctx.format {
|
inner: None,
|
||||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder {
|
ctx,
|
||||||
inner: Box::new(WebmRecorder::new(ctx.clone())?),
|
ctx2: None,
|
||||||
ctx,
|
pts: None,
|
||||||
pts: None,
|
check_failed: false,
|
||||||
},
|
})
|
||||||
#[cfg(feature = "hwcodec")]
|
|
||||||
_ => Recorder {
|
|
||||||
inner: Box::new(HwRecorder::new(ctx.clone())?),
|
|
||||||
ctx,
|
|
||||||
pts: None,
|
|
||||||
},
|
|
||||||
#[cfg(not(feature = "hwcodec"))]
|
|
||||||
_ => bail!("unsupported codec type"),
|
|
||||||
};
|
|
||||||
recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone()));
|
|
||||||
Ok(recorder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> {
|
fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
|
||||||
ctx.set_filename()?;
|
match self.ctx2 {
|
||||||
self.inner = match ctx.format {
|
Some(ref ctx2) => {
|
||||||
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => {
|
if ctx2.width != w || ctx2.height != h || ctx2.format != format {
|
||||||
Box::new(WebmRecorder::new(ctx.clone())?)
|
let mut ctx2 = RecorderContext2 {
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
format,
|
||||||
|
filename: Default::default(),
|
||||||
|
};
|
||||||
|
ctx2.set_filename(&self.ctx)?;
|
||||||
|
self.ctx2 = Some(ctx2);
|
||||||
|
self.inner = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "hwcodec")]
|
None => {
|
||||||
_ => Box::new(HwRecorder::new(ctx.clone())?),
|
let mut ctx2 = RecorderContext2 {
|
||||||
#[cfg(not(feature = "hwcodec"))]
|
width: w,
|
||||||
_ => bail!("unsupported codec type"),
|
height: h,
|
||||||
|
format,
|
||||||
|
filename: Default::default(),
|
||||||
|
};
|
||||||
|
ctx2.set_filename(&self.ctx)?;
|
||||||
|
self.ctx2 = Some(ctx2);
|
||||||
|
self.inner = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(ctx2) = &self.ctx2 else {
|
||||||
|
bail!("ctx2 is None");
|
||||||
};
|
};
|
||||||
self.ctx = ctx;
|
if self.inner.is_none() {
|
||||||
self.pts = None;
|
self.inner = match format {
|
||||||
self.send_state(RecordState::NewFile(self.ctx.filename.clone()));
|
CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new(
|
||||||
|
WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?,
|
||||||
|
)),
|
||||||
|
#[cfg(feature = "hwcodec")]
|
||||||
|
_ => Some(Box::new(HwRecorder::new(
|
||||||
|
self.ctx.clone(),
|
||||||
|
(*ctx2).clone(),
|
||||||
|
)?)),
|
||||||
|
#[cfg(not(feature = "hwcodec"))]
|
||||||
|
_ => bail!("unsupported codec type"),
|
||||||
|
};
|
||||||
|
self.pts = None;
|
||||||
|
self.send_state(RecordState::NewFile(ctx2.filename.clone()));
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_message(&mut self, msg: &Message) {
|
pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) {
|
||||||
if let Some(message::Union::VideoFrame(vf)) = &msg.union {
|
if let Some(message::Union::VideoFrame(vf)) = &msg.union {
|
||||||
if let Some(frame) = &vf.union {
|
if let Some(frame) = &vf.union {
|
||||||
self.write_frame(frame).ok();
|
self.write_frame(frame, w, h).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> {
|
pub fn write_frame(
|
||||||
|
&mut self,
|
||||||
|
frame: &video_frame::Union,
|
||||||
|
w: usize,
|
||||||
|
h: usize,
|
||||||
|
) -> ResultType<()> {
|
||||||
|
if self.check_failed {
|
||||||
|
bail!("check failed");
|
||||||
|
}
|
||||||
|
let format = CodecFormat::from(frame);
|
||||||
|
if format == CodecFormat::Unknown {
|
||||||
|
bail!("unsupported frame type");
|
||||||
|
}
|
||||||
|
let res = self.check(w, h, format);
|
||||||
|
if res.is_err() {
|
||||||
|
self.check_failed = true;
|
||||||
|
log::error!("check failed: {:?}", res);
|
||||||
|
res?;
|
||||||
|
}
|
||||||
match frame {
|
match frame {
|
||||||
video_frame::Union::Vp8s(vp8s) => {
|
video_frame::Union::Vp8s(vp8s) => {
|
||||||
if self.ctx.format != CodecFormat::VP8 {
|
|
||||||
self.change(RecorderContext {
|
|
||||||
format: CodecFormat::VP8,
|
|
||||||
..self.ctx.clone()
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
for f in vp8s.frames.iter() {
|
for f in vp8s.frames.iter() {
|
||||||
self.check_pts(f.pts)?;
|
self.check_pts(f.pts, w, h, format)?;
|
||||||
self.write_video(f);
|
self.as_mut().map(|x| x.write_video(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
video_frame::Union::Vp9s(vp9s) => {
|
video_frame::Union::Vp9s(vp9s) => {
|
||||||
if self.ctx.format != CodecFormat::VP9 {
|
|
||||||
self.change(RecorderContext {
|
|
||||||
format: CodecFormat::VP9,
|
|
||||||
..self.ctx.clone()
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
for f in vp9s.frames.iter() {
|
for f in vp9s.frames.iter() {
|
||||||
self.check_pts(f.pts)?;
|
self.check_pts(f.pts, w, h, format)?;
|
||||||
self.write_video(f);
|
self.as_mut().map(|x| x.write_video(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
video_frame::Union::Av1s(av1s) => {
|
video_frame::Union::Av1s(av1s) => {
|
||||||
if self.ctx.format != CodecFormat::AV1 {
|
|
||||||
self.change(RecorderContext {
|
|
||||||
format: CodecFormat::AV1,
|
|
||||||
..self.ctx.clone()
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
for f in av1s.frames.iter() {
|
for f in av1s.frames.iter() {
|
||||||
self.check_pts(f.pts)?;
|
self.check_pts(f.pts, w, h, format)?;
|
||||||
self.write_video(f);
|
self.as_mut().map(|x| x.write_video(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "hwcodec")]
|
#[cfg(feature = "hwcodec")]
|
||||||
video_frame::Union::H264s(h264s) => {
|
video_frame::Union::H264s(h264s) => {
|
||||||
if self.ctx.format != CodecFormat::H264 {
|
|
||||||
self.change(RecorderContext {
|
|
||||||
format: CodecFormat::H264,
|
|
||||||
..self.ctx.clone()
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
for f in h264s.frames.iter() {
|
for f in h264s.frames.iter() {
|
||||||
self.check_pts(f.pts)?;
|
self.check_pts(f.pts, w, h, format)?;
|
||||||
self.write_video(f);
|
self.as_mut().map(|x| x.write_video(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "hwcodec")]
|
#[cfg(feature = "hwcodec")]
|
||||||
video_frame::Union::H265s(h265s) => {
|
video_frame::Union::H265s(h265s) => {
|
||||||
if self.ctx.format != CodecFormat::H265 {
|
|
||||||
self.change(RecorderContext {
|
|
||||||
format: CodecFormat::H265,
|
|
||||||
..self.ctx.clone()
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
for f in h265s.frames.iter() {
|
for f in h265s.frames.iter() {
|
||||||
self.check_pts(f.pts)?;
|
self.check_pts(f.pts, w, h, format)?;
|
||||||
self.write_video(f);
|
self.as_mut().map(|x| x.write_video(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => bail!("unsupported frame type"),
|
_ => bail!("unsupported frame type"),
|
||||||
@@ -214,13 +230,21 @@ impl Recorder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_pts(&mut self, pts: i64) -> ResultType<()> {
|
fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> ResultType<()> {
|
||||||
// https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c
|
// https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c
|
||||||
let old_pts = self.pts;
|
let old_pts = self.pts;
|
||||||
self.pts = Some(pts);
|
self.pts = Some(pts);
|
||||||
if old_pts.clone().unwrap_or_default() > pts {
|
if old_pts.clone().unwrap_or_default() > pts {
|
||||||
log::info!("pts {:?} -> {}, change record filename", old_pts, pts);
|
log::info!("pts {:?} -> {}, change record filename", old_pts, pts);
|
||||||
self.change(self.ctx.clone())?;
|
self.inner = None;
|
||||||
|
self.ctx2 = None;
|
||||||
|
let res = self.check(w, h, format);
|
||||||
|
if res.is_err() {
|
||||||
|
self.check_failed = true;
|
||||||
|
log::error!("check failed: {:?}", res);
|
||||||
|
res?;
|
||||||
|
}
|
||||||
|
self.pts = Some(pts);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -234,21 +258,22 @@ struct WebmRecorder {
|
|||||||
vt: VideoTrack,
|
vt: VideoTrack,
|
||||||
webm: Option<Segment<Writer<File>>>,
|
webm: Option<Segment<Writer<File>>>,
|
||||||
ctx: RecorderContext,
|
ctx: RecorderContext,
|
||||||
|
ctx2: RecorderContext2,
|
||||||
key: bool,
|
key: bool,
|
||||||
written: bool,
|
written: bool,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecorderApi for WebmRecorder {
|
impl RecorderApi for WebmRecorder {
|
||||||
fn new(ctx: RecorderContext) -> ResultType<Self> {
|
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
||||||
let out = match {
|
let out = match {
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create_new(true)
|
.create_new(true)
|
||||||
.open(&ctx.filename)
|
.open(&ctx2.filename)
|
||||||
} {
|
} {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?,
|
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?,
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
};
|
};
|
||||||
let mut webm = match mux::Segment::new(mux::Writer::new(out)) {
|
let mut webm = match mux::Segment::new(mux::Writer::new(out)) {
|
||||||
@@ -256,18 +281,18 @@ impl RecorderApi for WebmRecorder {
|
|||||||
None => bail!("Failed to create webm mux"),
|
None => bail!("Failed to create webm mux"),
|
||||||
};
|
};
|
||||||
let vt = webm.add_video_track(
|
let vt = webm.add_video_track(
|
||||||
ctx.width as _,
|
ctx2.width as _,
|
||||||
ctx.height as _,
|
ctx2.height as _,
|
||||||
None,
|
None,
|
||||||
if ctx.format == CodecFormat::VP9 {
|
if ctx2.format == CodecFormat::VP9 {
|
||||||
mux::VideoCodecId::VP9
|
mux::VideoCodecId::VP9
|
||||||
} else if ctx.format == CodecFormat::VP8 {
|
} else if ctx2.format == CodecFormat::VP8 {
|
||||||
mux::VideoCodecId::VP8
|
mux::VideoCodecId::VP8
|
||||||
} else {
|
} else {
|
||||||
mux::VideoCodecId::AV1
|
mux::VideoCodecId::AV1
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if ctx.format == CodecFormat::AV1 {
|
if ctx2.format == CodecFormat::AV1 {
|
||||||
// [129, 8, 12, 0] in 3.6.0, but zero works
|
// [129, 8, 12, 0] in 3.6.0, but zero works
|
||||||
let codec_private = vec![0, 0, 0, 0];
|
let codec_private = vec![0, 0, 0, 0];
|
||||||
if !webm.set_codec_private(vt.track_number(), &codec_private) {
|
if !webm.set_codec_private(vt.track_number(), &codec_private) {
|
||||||
@@ -278,6 +303,7 @@ impl RecorderApi for WebmRecorder {
|
|||||||
vt,
|
vt,
|
||||||
webm: Some(webm),
|
webm: Some(webm),
|
||||||
ctx,
|
ctx,
|
||||||
|
ctx2,
|
||||||
key: false,
|
key: false,
|
||||||
written: false,
|
written: false,
|
||||||
start: Instant::now(),
|
start: Instant::now(),
|
||||||
@@ -307,7 +333,7 @@ impl Drop for WebmRecorder {
|
|||||||
let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None));
|
let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None));
|
||||||
let mut state = RecordState::WriteTail;
|
let mut state = RecordState::WriteTail;
|
||||||
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
||||||
std::fs::remove_file(&self.ctx.filename).ok();
|
std::fs::remove_file(&self.ctx2.filename).ok();
|
||||||
state = RecordState::RemoveFile;
|
state = RecordState::RemoveFile;
|
||||||
}
|
}
|
||||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||||
@@ -318,6 +344,7 @@ impl Drop for WebmRecorder {
|
|||||||
struct HwRecorder {
|
struct HwRecorder {
|
||||||
muxer: Muxer,
|
muxer: Muxer,
|
||||||
ctx: RecorderContext,
|
ctx: RecorderContext,
|
||||||
|
ctx2: RecorderContext2,
|
||||||
written: bool,
|
written: bool,
|
||||||
key: bool,
|
key: bool,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
@@ -325,18 +352,19 @@ struct HwRecorder {
|
|||||||
|
|
||||||
#[cfg(feature = "hwcodec")]
|
#[cfg(feature = "hwcodec")]
|
||||||
impl RecorderApi for HwRecorder {
|
impl RecorderApi for HwRecorder {
|
||||||
fn new(ctx: RecorderContext) -> ResultType<Self> {
|
fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType<Self> {
|
||||||
let muxer = Muxer::new(MuxContext {
|
let muxer = Muxer::new(MuxContext {
|
||||||
filename: ctx.filename.clone(),
|
filename: ctx2.filename.clone(),
|
||||||
width: ctx.width,
|
width: ctx2.width,
|
||||||
height: ctx.height,
|
height: ctx2.height,
|
||||||
is265: ctx.format == CodecFormat::H265,
|
is265: ctx2.format == CodecFormat::H265,
|
||||||
framerate: crate::hwcodec::DEFAULT_FPS as _,
|
framerate: crate::hwcodec::DEFAULT_FPS as _,
|
||||||
})
|
})
|
||||||
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
||||||
Ok(HwRecorder {
|
Ok(HwRecorder {
|
||||||
muxer,
|
muxer,
|
||||||
ctx,
|
ctx,
|
||||||
|
ctx2,
|
||||||
written: false,
|
written: false,
|
||||||
key: false,
|
key: false,
|
||||||
start: Instant::now(),
|
start: Instant::now(),
|
||||||
@@ -365,7 +393,7 @@ impl Drop for HwRecorder {
|
|||||||
self.muxer.write_tail().ok();
|
self.muxer.write_tail().ok();
|
||||||
let mut state = RecordState::WriteTail;
|
let mut state = RecordState::WriteTail;
|
||||||
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
if !self.written || self.start.elapsed().as_secs() < MIN_SECS {
|
||||||
std::fs::remove_file(&self.ctx.filename).ok();
|
std::fs::remove_file(&self.ctx2.filename).ok();
|
||||||
state = RecordState::RemoveFile;
|
state = RecordState::RemoveFile;
|
||||||
}
|
}
|
||||||
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
self.ctx.tx.as_ref().map(|tx| tx.send(state));
|
||||||
|
|||||||
@@ -101,7 +101,12 @@ impl EncoderApi for VRamEncoder {
|
|||||||
frame: EncodeInput,
|
frame: EncodeInput,
|
||||||
ms: i64,
|
ms: i64,
|
||||||
) -> ResultType<hbb_common::message_proto::VideoFrame> {
|
) -> ResultType<hbb_common::message_proto::VideoFrame> {
|
||||||
let texture = frame.texture()?;
|
let (texture, rotation) = frame.texture()?;
|
||||||
|
if rotation != 0 {
|
||||||
|
// to-do: support rotation
|
||||||
|
// Both the encoder and display(w,h) information need to be changed.
|
||||||
|
bail!("rotation not supported");
|
||||||
|
}
|
||||||
let mut vf = VideoFrame::new();
|
let mut vf = VideoFrame::new();
|
||||||
let mut frames = Vec::new();
|
let mut frames = Vec::new();
|
||||||
for frame in self
|
for frame in self
|
||||||
|
|||||||
@@ -253,7 +253,17 @@ impl Capturer {
|
|||||||
|
|
||||||
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<Frame<'a>> {
|
pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<Frame<'a>> {
|
||||||
if self.output_texture {
|
if self.output_texture {
|
||||||
Ok(Frame::Texture(self.get_texture(timeout)?))
|
let rotation = match self.display.rotation() {
|
||||||
|
DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0,
|
||||||
|
DXGI_MODE_ROTATION_ROTATE90 => 90,
|
||||||
|
DXGI_MODE_ROTATION_ROTATE180 => 180,
|
||||||
|
DXGI_MODE_ROTATION_ROTATE270 => 270,
|
||||||
|
_ => {
|
||||||
|
// Unsupported rotation, try anyway
|
||||||
|
0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Frame::Texture((self.get_texture(timeout)?, rotation)))
|
||||||
} else {
|
} else {
|
||||||
let width = self.width;
|
let width = self.width;
|
||||||
let height = self.height;
|
let height = self.height;
|
||||||
|
|||||||
@@ -27,39 +27,40 @@ use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_porta
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref RDP_RESPONSE: Mutex<Option<RdpResponse>> = Mutex::new(None);
|
pub static ref RDP_SESSION_INFO: Mutex<Option<RdpSessionInfo>> = Mutex::new(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn close_session() {
|
pub fn close_session() {
|
||||||
let _ = RDP_RESPONSE.lock().unwrap().take();
|
let _ = RDP_SESSION_INFO.lock().unwrap().take();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_rdp_session_hold() -> bool {
|
pub fn is_rdp_session_hold() -> bool {
|
||||||
RDP_RESPONSE.lock().unwrap().is_some()
|
RDP_SESSION_INFO.lock().unwrap().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_close_session() {
|
pub fn try_close_session() {
|
||||||
let mut rdp_res = RDP_RESPONSE.lock().unwrap();
|
let mut rdp_info = RDP_SESSION_INFO.lock().unwrap();
|
||||||
let mut close = false;
|
let mut close = false;
|
||||||
if let Some(rdp_res) = &*rdp_res {
|
if let Some(rdp_info) = &*rdp_info {
|
||||||
// If is server running and restore token is supported, there's no need to keep the session.
|
// If is server running and restore token is supported, there's no need to keep the session.
|
||||||
if is_server_running() && rdp_res.is_support_restore_token {
|
if is_server_running() && rdp_info.is_support_restore_token {
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if close {
|
if close {
|
||||||
*rdp_res = None;
|
*rdp_info = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RdpResponse {
|
pub struct RdpSessionInfo {
|
||||||
pub conn: Arc<SyncConnection>,
|
pub conn: Arc<SyncConnection>,
|
||||||
pub streams: Vec<PwStreamInfo>,
|
pub streams: Vec<PwStreamInfo>,
|
||||||
pub fd: OwnedFd,
|
pub fd: OwnedFd,
|
||||||
pub session: dbus::Path<'static>,
|
pub session: dbus::Path<'static>,
|
||||||
pub is_support_restore_token: bool,
|
pub is_support_restore_token: bool,
|
||||||
|
pub resolution: Arc<Mutex<Option<(usize, usize)>>>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct PwStreamInfo {
|
pub struct PwStreamInfo {
|
||||||
@@ -69,6 +70,12 @@ pub struct PwStreamInfo {
|
|||||||
size: (usize, usize),
|
size: (usize, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PwStreamInfo {
|
||||||
|
pub fn get_size(&self) -> (usize, usize) {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DBusError(String);
|
pub struct DBusError(String);
|
||||||
|
|
||||||
@@ -105,24 +112,31 @@ pub struct PipeWireCapturable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PipeWireCapturable {
|
impl PipeWireCapturable {
|
||||||
fn new(conn: Arc<SyncConnection>, fd: OwnedFd, stream: PwStreamInfo) -> Self {
|
fn new(
|
||||||
|
conn: Arc<SyncConnection>,
|
||||||
|
fd: OwnedFd,
|
||||||
|
resolution: Arc<Mutex<Option<(usize, usize)>>>,
|
||||||
|
stream: PwStreamInfo,
|
||||||
|
) -> Self {
|
||||||
// alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling
|
// alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling
|
||||||
// https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244
|
// https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244
|
||||||
let res = get_res(Self {
|
let size = get_res(Self {
|
||||||
dbus_conn: conn.clone(),
|
dbus_conn: conn.clone(),
|
||||||
fd: fd.clone(),
|
fd: fd.clone(),
|
||||||
path: stream.path,
|
path: stream.path,
|
||||||
source_type: stream.source_type,
|
source_type: stream.source_type,
|
||||||
position: stream.position,
|
position: stream.position,
|
||||||
size: stream.size,
|
size: stream.size,
|
||||||
});
|
})
|
||||||
|
.unwrap_or(stream.size);
|
||||||
|
*resolution.lock().unwrap() = Some(size);
|
||||||
Self {
|
Self {
|
||||||
dbus_conn: conn,
|
dbus_conn: conn,
|
||||||
fd,
|
fd,
|
||||||
path: stream.path,
|
path: stream.path,
|
||||||
source_type: stream.source_type,
|
source_type: stream.source_type,
|
||||||
position: stream.position,
|
position: stream.position,
|
||||||
size: res.unwrap_or(stream.size),
|
size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -813,7 +827,7 @@ fn on_start_response(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
||||||
let mut rdp_connection = match RDP_RESPONSE.lock() {
|
let mut rdp_connection = match RDP_SESSION_INFO.lock() {
|
||||||
Ok(conn) => conn,
|
Ok(conn) => conn,
|
||||||
Err(err) => return Err(Box::new(err)),
|
Err(err) => return Err(Box::new(err)),
|
||||||
};
|
};
|
||||||
@@ -822,28 +836,36 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
|
|||||||
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?;
|
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?;
|
||||||
let conn = Arc::new(conn);
|
let conn = Arc::new(conn);
|
||||||
|
|
||||||
let rdp_res = RdpResponse {
|
let rdp_info = RdpSessionInfo {
|
||||||
conn,
|
conn,
|
||||||
streams,
|
streams,
|
||||||
fd,
|
fd,
|
||||||
session,
|
session,
|
||||||
is_support_restore_token,
|
is_support_restore_token,
|
||||||
|
resolution: Arc::new(Mutex::new(None)),
|
||||||
};
|
};
|
||||||
*rdp_connection = Some(rdp_res);
|
*rdp_connection = Some(rdp_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rdp_res = match rdp_connection.as_ref() {
|
let rdp_info = match rdp_connection.as_ref() {
|
||||||
Some(res) => res,
|
Some(res) => res,
|
||||||
None => {
|
None => {
|
||||||
return Err(Box::new(DBusError("RDP response is None.".into())));
|
return Err(Box::new(DBusError("RDP response is None.".into())));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(rdp_res
|
Ok(rdp_info
|
||||||
.streams
|
.streams
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| PipeWireCapturable::new(rdp_res.conn.clone(), rdp_res.fd.clone(), s))
|
.map(|s| {
|
||||||
|
PipeWireCapturable::new(
|
||||||
|
rdp_info.conn.clone(),
|
||||||
|
rdp_info.fd.clone(),
|
||||||
|
rdp_info.resolution.clone(),
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use hbb_common::libc;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@@ -99,11 +100,16 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error>
|
|||||||
if reply.is_null() {
|
if reply.is_null() {
|
||||||
// TODO: Should seperate SHM disabled from SHM not supported?
|
// TODO: Should seperate SHM disabled from SHM not supported?
|
||||||
return Err(Error::UnsupportedExtension);
|
return Err(Error::UnsupportedExtension);
|
||||||
} else if e.is_null() {
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
} else {
|
||||||
// TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here?
|
// https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229
|
||||||
return Err(Error::Generic);
|
libc::free(reply as *mut _);
|
||||||
|
if e.is_null() {
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
libc::free(e as *mut _);
|
||||||
|
// TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here?
|
||||||
|
return Err(Error::Generic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ case $1 in
|
|||||||
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true
|
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true
|
||||||
|
|
||||||
# workaround temp dev build between 1.1.9 and 1.2.0
|
# workaround temp dev build between 1.1.9 and 1.2.0
|
||||||
ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l)
|
serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1)
|
||||||
waylandSupportVersion=21
|
if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
|
||||||
if [ "$ubuntuVersion" != "" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ]
|
|
||||||
then
|
then
|
||||||
serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1)
|
systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true
|
||||||
if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
|
|
||||||
then
|
|
||||||
systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true
|
rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.3.0
|
pkgver=1.3.2
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE policyconfig PUBLIC
|
|
||||||
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
|
||||||
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
|
||||||
<policyconfig>
|
|
||||||
<vendor>RustDesk</vendor>
|
|
||||||
<vendor_url>https://rustdesk.com/</vendor_url>
|
|
||||||
<icon_name>rustdesk</icon_name>
|
|
||||||
<action id="com.rustdesk.RustDesk.options">
|
|
||||||
<description>Change RustDesk options</description>
|
|
||||||
<message>Authentication is required to change RustDesk options</message>
|
|
||||||
<message xml:lang="zh_CN">要更改RustDesk选项, 需要您先通过身份验证</message>
|
|
||||||
<message xml:lang="zh_TW">要變更RustDesk選項, 需要您先通過身份驗證</message>
|
|
||||||
<message xml:lang="de">Authentifizierung zum Ändern der RustDesk-Optionen</message>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/share/rustdesk/files/polkit</annotate>
|
|
||||||
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
|
|
||||||
<defaults>
|
|
||||||
<allow_any>auth_admin</allow_any>
|
|
||||||
<allow_inactive>auth_admin</allow_inactive>
|
|
||||||
<allow_active>auth_admin</allow_active>
|
|
||||||
</defaults>
|
|
||||||
</action>
|
|
||||||
</policyconfig>
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.3.0
|
Version: 1.3.2
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libappindicator-gtk3 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||||
|
Recommends: libayatana-appindicator3-1
|
||||||
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
|
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
|
||||||
|
|
||||||
%description
|
%description
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.3.0
|
Version: 1.3.2
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator-gtk3 libvdpau libva pam gstreamer1-plugins-base
|
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau libva pam gstreamer1-plugins-base
|
||||||
|
Recommends: libayatana-appindicator-gtk3
|
||||||
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
|
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
|
||||||
|
|
||||||
%description
|
%description
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ Version: 1.1.9
|
|||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||||
|
Recommends: libayatana-appindicator3-1
|
||||||
|
|
||||||
%description
|
%description
|
||||||
The best open-source remote desktop client software, written in Rust.
|
The best open-source remote desktop client software, written in Rust.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.3.0
|
Version: 1.3.2
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator libvdpau1 libva2 pam gstreamer1-plugins-base
|
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau1 libva2 pam gstreamer1-plugins-base
|
||||||
|
Recommends: libayatana-appindicator-gtk3
|
||||||
|
|
||||||
%description
|
%description
|
||||||
The best open-source remote desktop client software, written in Rust.
|
The best open-source remote desktop client software, written in Rust.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user