mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-21 09:08:35 +08:00
Compare commits
238 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a516f01feb | ||
|
|
2e314bf032 | ||
|
|
b93d4ce3fc | ||
|
|
21bcfd173d | ||
|
|
3d5262c36f | ||
|
|
cfd801c5d6 | ||
|
|
216a72592d | ||
|
|
ddd3401bd7 | ||
|
|
47139edd81 | ||
|
|
c6e3f60a6b | ||
|
|
88a99211f3 | ||
|
|
d08c335fdf | ||
|
|
e5ec6957fe | ||
|
|
e20f5dd001 | ||
|
|
e1a6ccc100 | ||
|
|
cc288272d3 | ||
|
|
49ce4edb8a | ||
|
|
29c3b29bda | ||
|
|
8a8f708c3e | ||
|
|
c5038b1a78 | ||
|
|
f4c038ea93 | ||
|
|
d9ea717056 | ||
|
|
40af9dc78b | ||
|
|
81fc22a156 | ||
|
|
2e7bd26e4c | ||
|
|
179b562472 | ||
|
|
ab246fdcbf | ||
|
|
d65d3b7326 | ||
|
|
9f9a22ec63 | ||
|
|
a8f1a66043 | ||
|
|
0b3e7bf33e | ||
|
|
c358399eca | ||
|
|
cacca7295c | ||
|
|
d2e98cc620 | ||
|
|
2e81bcb447 | ||
|
|
cbca0eb340 | ||
|
|
9380f33d7c | ||
|
|
519539ed0a | ||
|
|
1f2a75fbd8 | ||
|
|
51055a7e5b | ||
|
|
13effe7f14 | ||
|
|
943f96ef8c | ||
|
|
260a82ee5c | ||
|
|
a2792d1527 | ||
|
|
2922ebe22a | ||
|
|
1e6944b380 | ||
|
|
993862c103 | ||
|
|
c8cd564e69 | ||
|
|
a4cd64f0d5 | ||
|
|
f0ca4b9fee | ||
|
|
aa3402b44a | ||
|
|
26ebd0deb9 | ||
|
|
4150036589 | ||
|
|
7a1157f1b0 | ||
|
|
3bd34bf0b9 | ||
|
|
5f29016861 | ||
|
|
e40243b55d | ||
|
|
dbbbd08934 | ||
|
|
29e12b84a9 | ||
|
|
04c0f66ca9 | ||
|
|
ec28567362 | ||
|
|
d4377a13c5 | ||
|
|
39e713838f | ||
|
|
75a4671bda | ||
|
|
827efabbc0 | ||
|
|
532fe6aefb | ||
|
|
ae339f039d | ||
|
|
bf390611ab | ||
|
|
e3f6829d02 | ||
|
|
832002a10f | ||
|
|
d335cdbb0c | ||
|
|
6a5d5875c8 | ||
|
|
cf06d1028f | ||
|
|
fd178a7b6c | ||
|
|
f3a2733d75 | ||
|
|
55de573a01 | ||
|
|
40239a1c41 | ||
|
|
c68ce7dd84 | ||
|
|
690a2c8399 | ||
|
|
4b4fd94f3e | ||
|
|
5abe42f66c | ||
|
|
48aec6484c | ||
|
|
a946d4d0c9 | ||
|
|
24f4b94082 | ||
|
|
aa1e122532 | ||
|
|
d400999b9c | ||
|
|
1d416f6626 | ||
|
|
9d9741f18e | ||
|
|
50aa8e12ad | ||
|
|
5931af460e | ||
|
|
fc607d6789 | ||
|
|
529e70910d | ||
|
|
f300d797e2 | ||
|
|
e3cce2824d | ||
|
|
f34b8411a7 | ||
|
|
8745fcbb6a | ||
|
|
2a0fd55af7 | ||
|
|
da70cbcdda | ||
|
|
921b64e1e0 | ||
|
|
c0de0aa108 | ||
|
|
715d475f49 | ||
|
|
e3f09b3ec6 | ||
|
|
0a5fafb84f | ||
|
|
4e084c5ee0 | ||
|
|
d1fe617670 | ||
|
|
7744bdbbe0 | ||
|
|
e1329c8157 | ||
|
|
f31e60af5b | ||
|
|
ed18e3c786 | ||
|
|
579e0fac36 | ||
|
|
92752765ba | ||
|
|
071f51cf6f | ||
|
|
dde3cce120 | ||
|
|
bb1b9858d5 | ||
|
|
a31c27be73 | ||
|
|
85ae3916cb | ||
|
|
cc9b7e64eb | ||
|
|
5e22a49e49 | ||
|
|
07cf1b4db5 | ||
|
|
f6ab5cdcb2 | ||
|
|
b477aded0b | ||
|
|
65318efd67 | ||
|
|
dbd195a46e | ||
|
|
0651ad492f | ||
|
|
a3c5adb1f4 | ||
|
|
a771abcdc2 | ||
|
|
b8b3a089f3 | ||
|
|
8f00067266 | ||
|
|
83bf067d18 | ||
|
|
1729ee337f | ||
|
|
57834840b8 | ||
|
|
99d7b62d79 | ||
|
|
6625aca994 | ||
|
|
ce56be6507 | ||
|
|
fd69b14623 | ||
|
|
7521bbe15f | ||
|
|
3c6ddd7403 | ||
|
|
6820e2f4c7 | ||
|
|
77f3ebaf1a | ||
|
|
e7e244d4f2 | ||
|
|
f4c40d733e | ||
|
|
049c334db3 | ||
|
|
6197832317 | ||
|
|
2fd53f9825 | ||
|
|
ae16b8975b | ||
|
|
171177c76f | ||
|
|
ade1d8c0c7 | ||
|
|
9a194f0850 | ||
|
|
76d5a8b205 | ||
|
|
bc6ce6c7ee | ||
|
|
025cdfa25b | ||
|
|
2f432e941d | ||
|
|
96edca8f74 | ||
|
|
51b250435d | ||
|
|
5a2121501d | ||
|
|
877b3e2ce5 | ||
|
|
421ddc0016 | ||
|
|
2662abc5a3 | ||
|
|
b3e1c8a907 | ||
|
|
2266fde26f | ||
|
|
e58e75eea9 | ||
|
|
eafebdba21 | ||
|
|
7bf5e69444 | ||
|
|
cb0dc46d08 | ||
|
|
9b8209b61b | ||
|
|
b6ba9978e3 | ||
|
|
0d1d7a9b87 | ||
|
|
508dd5b383 | ||
|
|
31a1b7a80b | ||
|
|
ba43424781 | ||
|
|
f899b2a962 | ||
|
|
2dd3d8c11e | ||
|
|
6eea425280 | ||
|
|
5e7d4fd2d6 | ||
|
|
d9fba50606 | ||
|
|
b6035fbbdf | ||
|
|
e67b694f06 | ||
|
|
2333ee2c07 | ||
|
|
61ccc2152e | ||
|
|
f6aca4ca8e | ||
|
|
1707987a7b | ||
|
|
85604dee79 | ||
|
|
a12969be30 | ||
|
|
9f91eada89 | ||
|
|
cb6a6aa42a | ||
|
|
4fec8abad4 | ||
|
|
35571dc8d7 | ||
|
|
a103b83647 | ||
|
|
e1e4bf599b | ||
|
|
cba8aaa410 | ||
|
|
8ced4ddaa2 | ||
|
|
15404ecab4 | ||
|
|
97772f9ac5 | ||
|
|
764fbe2c9d | ||
|
|
e03344d85b | ||
|
|
0e98a51775 | ||
|
|
0a1d3c4afb | ||
|
|
fd9b5f3c57 | ||
|
|
73f6afd4c0 | ||
|
|
50dd2b3aad | ||
|
|
c0e9445602 | ||
|
|
541d9c6b86 | ||
|
|
8c91e5c5ca | ||
|
|
19d1605d8c | ||
|
|
0faf82f109 | ||
|
|
ee5314de20 | ||
|
|
7e8d3bd2ac | ||
|
|
9750e1409c | ||
|
|
67d4e061fb | ||
|
|
f67f2be0cb | ||
|
|
aa42bf548e | ||
|
|
d679e8fa7d | ||
|
|
1357ef5d6f | ||
|
|
30a5d1e0e1 | ||
|
|
9f0985c842 | ||
|
|
be06c0d738 | ||
|
|
f0f50f0f03 | ||
|
|
1850d32f49 | ||
|
|
3999d498be | ||
|
|
bbdce8d57b | ||
|
|
baf70da2fe | ||
|
|
b967d496cc | ||
|
|
2aef79688b | ||
|
|
0451a1c45f | ||
|
|
f7e9057a39 | ||
|
|
d73e0e1e5a | ||
|
|
39dbd89287 | ||
|
|
c04f460bbd | ||
|
|
79a1f888d6 | ||
|
|
57d1b1ecc4 | ||
|
|
8a4a2b5732 | ||
|
|
51a60a7eed | ||
|
|
b26acde450 | ||
|
|
2de81045ea | ||
|
|
a72a8906b0 | ||
|
|
614086a216 | ||
|
|
2ffc2ad85b | ||
|
|
eef091d4e8 |
@@ -1,8 +1,14 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||
[target.i686-pc-windows-msvc]
|
||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
|
||||
[target.'cfg(target_os="macos")']
|
||||
rustflags = [
|
||||
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
|
||||
]
|
||||
#[target.'cfg(target_os="linux")']
|
||||
# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
|
||||
# this is unlikely to help also, because the other so files still use libc dynamically
|
||||
#rustflags = [
|
||||
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
|
||||
#]
|
||||
|
||||
2
.github/workflows/bridge.yml
vendored
2
.github/workflows/bridge.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.16.9"
|
||||
FLUTTER_VERSION: "3.19.6"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -4,9 +4,9 @@ env:
|
||||
# MIN_SUPPORTED_RUST_VERSION: "1.46.0"
|
||||
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2023.10.19
|
||||
# vcpkg version: 2024.06.15
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -112,6 +112,8 @@ jobs:
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
|
||||
7
.github/workflows/fdroid.yml
vendored
7
.github/workflows/fdroid.yml
vendored
@@ -19,7 +19,12 @@ jobs:
|
||||
steps:
|
||||
- name: Generate RustDesk version file
|
||||
run: |
|
||||
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
|
||||
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||
UPSTREAM_VERNAME="${GITHUB_REF##refs/tags/}"
|
||||
UPSTREAM_VERNAME="${UPSTREAM_VERNAME##v}"
|
||||
else
|
||||
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
|
||||
fi
|
||||
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
|
||||
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
||||
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
||||
|
||||
557
.github/workflows/flutter-build.yml
vendored
557
.github/workflows/flutter-build.yml
vendored
@@ -18,9 +18,11 @@ on:
|
||||
# in this file!
|
||||
|
||||
env:
|
||||
WIN_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||
SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||
CARGO_NDK_VERSION: "3.1.2"
|
||||
SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
|
||||
SCITER_NASM_DEBVERSION: "2.14-1"
|
||||
LLVM_VERSION: "15.0.6"
|
||||
FLUTTER_VERSION: "3.19.6"
|
||||
ANDROID_FLUTTER_VERSION: "3.13.9" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem).
|
||||
@@ -29,10 +31,10 @@ env:
|
||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.06.15
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
VERSION: "1.2.7"
|
||||
NDK_VERSION: "r26d"
|
||||
# vcpkg version: 2024.07.12
|
||||
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
|
||||
VERSION: "1.3.1"
|
||||
NDK_VERSION: "r27"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||
@@ -56,7 +58,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
build-for-windows-flutter:
|
||||
name: ${{ matrix.job.target }}
|
||||
name: ${{ matrix.job.target }}
|
||||
needs: [build-RustDeskTempTopMostWindow]
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
@@ -65,7 +67,12 @@ jobs:
|
||||
job:
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, vcpkg-triplet: x64-windows-static }
|
||||
- {
|
||||
target: x86_64-pc-windows-msvc,
|
||||
os: windows-2022,
|
||||
arch: x86_64,
|
||||
vcpkg-triplet: x64-windows-static,
|
||||
}
|
||||
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
@@ -93,7 +100,7 @@ jobs:
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.WIN_RUST_VERSION }}
|
||||
toolchain: ${{ env.SCITER_RUST_VERSION }}
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: "rustfmt"
|
||||
|
||||
@@ -113,10 +120,25 @@ jobs:
|
||||
with:
|
||||
vcpkgDirectory: C:\vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
env:
|
||||
VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }}
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||
if ! $VCPKG_ROOT/vcpkg \
|
||||
install \
|
||||
--triplet ${{ matrix.job.vcpkg-triplet }} \
|
||||
--x-install-root="$VCPKG_ROOT/installed"; then
|
||||
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
|
||||
echo "$_1:"
|
||||
echo "======"
|
||||
cat "$_1"
|
||||
echo "======"
|
||||
echo ""
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
@@ -156,7 +178,7 @@ jobs:
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
|
||||
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
|
||||
path: rustdesk
|
||||
|
||||
- name: Sign rustdesk files
|
||||
@@ -219,7 +241,12 @@ jobs:
|
||||
job:
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||
- { target: i686-pc-windows-msvc, os: windows-2022, arch: x86, vcpkg-triplet: x86-windows-static }
|
||||
- {
|
||||
target: i686-pc-windows-msvc,
|
||||
os: windows-2022,
|
||||
arch: x86,
|
||||
vcpkg-triplet: x86-windows-static,
|
||||
}
|
||||
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
@@ -230,7 +257,7 @@ jobs:
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: rustdesk-org/install-llvm-action-32bit@master
|
||||
@@ -253,10 +280,25 @@ jobs:
|
||||
with:
|
||||
vcpkgDirectory: C:\vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
env:
|
||||
VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }}
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||
if ! $VCPKG_ROOT/vcpkg \
|
||||
install \
|
||||
--triplet ${{ matrix.job.vcpkg-triplet }} \
|
||||
--x-install-root="$VCPKG_ROOT/installed"; then
|
||||
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
|
||||
echo "$_1:"
|
||||
echo "======"
|
||||
cat "$_1"
|
||||
echo "======"
|
||||
echo ""
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
@@ -276,7 +318,7 @@ jobs:
|
||||
# Do not remove x64 files, because the user may run the 32bit version on a 64bit system.
|
||||
# Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64,
|
||||
# we need the x64 exe to install and uninstall the driver.
|
||||
rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat
|
||||
rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat
|
||||
mv ./usbmmidd_v2 ./Release || true
|
||||
|
||||
- name: find Runner.res
|
||||
@@ -341,7 +383,7 @@ jobs:
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
@@ -424,9 +466,9 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew install nasm
|
||||
brew install nasm yasm
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@@ -437,10 +479,23 @@ jobs:
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||
if ! $VCPKG_ROOT/vcpkg \
|
||||
install \
|
||||
--triplet ${{ matrix.job.vcpkg-triplet }} \
|
||||
--x-install-root="$VCPKG_ROOT/installed"; then
|
||||
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
|
||||
echo "$_1:"
|
||||
echo "======"
|
||||
cat "$_1"
|
||||
echo "======"
|
||||
echo ""
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install Rust toolchain
|
||||
@@ -506,7 +561,7 @@ jobs:
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
|
||||
|
||||
@@ -576,7 +631,7 @@ jobs:
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Import the codesign cert
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
@@ -651,10 +706,22 @@ jobs:
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||
if ! $VCPKG_ROOT/vcpkg \
|
||||
install \
|
||||
--x-install-root="$VCPKG_ROOT/installed"; then
|
||||
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
|
||||
echo "$_1:"
|
||||
echo "======"
|
||||
cat "$_1"
|
||||
echo "======"
|
||||
echo ""
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Show version information (Rust, cargo, Clang)
|
||||
shell: bash
|
||||
@@ -770,15 +837,35 @@ jobs:
|
||||
arch: aarch64,
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-20.04,
|
||||
openssl-arch: android-arm64,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
- {
|
||||
arch: armv7,
|
||||
target: armv7-linux-androideabi,
|
||||
os: ubuntu-20.04,
|
||||
openssl-arch: android-arm,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-linux-android,
|
||||
os: ubuntu-20.04,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: false
|
||||
docker-images: true
|
||||
swap-storage: false
|
||||
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
@@ -797,7 +884,7 @@ jobs:
|
||||
git \
|
||||
g++ \
|
||||
g++-multilib \
|
||||
libappindicator3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-10-dev \
|
||||
@@ -822,7 +909,7 @@ jobs:
|
||||
wget
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@@ -839,17 +926,34 @@ jobs:
|
||||
with:
|
||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
case ${{ matrix.job.target }} in
|
||||
aarch64-linux-android)
|
||||
./flutter/build_android_deps.sh arm64-v8a
|
||||
ANDROID_TARGET=arm64-v8a
|
||||
;;
|
||||
armv7-linux-androideabi)
|
||||
./flutter/build_android_deps.sh armeabi-v7a
|
||||
ANDROID_TARGET=armeabi-v7a
|
||||
;;
|
||||
x86_64-linux-android)
|
||||
ANDROID_TARGET=x86_64
|
||||
;;
|
||||
i686-linux-android)
|
||||
ANDROID_TARGET=x86
|
||||
;;
|
||||
esac
|
||||
if ! ./flutter/build_android_deps.sh "${ANDROID_TARGET}"; then
|
||||
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
|
||||
echo "$_1:"
|
||||
echo "======"
|
||||
cat "$_1"
|
||||
echo "======"
|
||||
echo ""
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Restore bridge files
|
||||
@@ -872,8 +976,11 @@ jobs:
|
||||
- name: fix android for flutter 3.13
|
||||
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||
run: |
|
||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
|
||||
cd flutter/lib
|
||||
cd flutter
|
||||
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'
|
||||
|
||||
- name: Build rustdesk lib
|
||||
@@ -894,8 +1001,24 @@ jobs:
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||
;;
|
||||
x86_64-linux-android)
|
||||
./flutter/ndk_x64.sh
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
|
||||
;;
|
||||
i686-linux-android)
|
||||
./flutter/ndk_x86.sh
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/x86
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload Rustdesk library to Artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: librustdesk.so.${{ matrix.job.target }}
|
||||
path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so
|
||||
|
||||
- name: Build rustdesk
|
||||
shell: bash
|
||||
env:
|
||||
@@ -911,8 +1034,8 @@ jobs:
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||
# build flutter
|
||||
pushd flutter
|
||||
flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||
flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm64 --split-per-abi
|
||||
mv build/app/outputs/flutter-apk/app-arm64-v8a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
|
||||
;;
|
||||
armv7-linux-androideabi)
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||
@@ -920,13 +1043,31 @@ jobs:
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||
# build flutter
|
||||
pushd flutter
|
||||
flutter build apk --release --target-platform android-arm --split-per-abi
|
||||
mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||
flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm --split-per-abi
|
||||
mv build/app/outputs/flutter-apk/app-armeabi-v7a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
|
||||
;;
|
||||
x86_64-linux-android)
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64
|
||||
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
|
||||
# build flutter
|
||||
pushd flutter
|
||||
flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x64 --split-per-abi
|
||||
mv build/app/outputs/flutter-apk/app-x86_64-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
|
||||
;;
|
||||
i686-linux-android)
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/x86
|
||||
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/
|
||||
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
|
||||
# build flutter
|
||||
pushd flutter
|
||||
flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi
|
||||
mv build/app/outputs/flutter-apk/app-x86-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
|
||||
;;
|
||||
esac
|
||||
popd
|
||||
mkdir -p signed-apk; pushd signed-apk
|
||||
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk .
|
||||
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk .
|
||||
|
||||
- uses: r0adkll/sign-android-release@v1
|
||||
name: Sign app APK
|
||||
@@ -967,9 +1108,185 @@ jobs:
|
||||
files: |
|
||||
signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||
|
||||
build-rustdesk-android-universal:
|
||||
needs: [build-rustdesk-android]
|
||||
name: build rustdesk android universal apk
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
reltype: release
|
||||
x86_target: "" # can be ",android-x86"
|
||||
suffix: ""
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: false
|
||||
docker-images: true
|
||||
swap-storage: false
|
||||
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
clang \
|
||||
cmake \
|
||||
curl \
|
||||
gcc-multilib \
|
||||
git \
|
||||
g++ \
|
||||
g++-multilib \
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-10-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-10-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-11-jdk-headless \
|
||||
pkg-config \
|
||||
tree \
|
||||
wget
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Download Rustdesk library from Artifacts
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: librustdesk.so.aarch64-linux-android
|
||||
path: ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||
|
||||
- name: Download Rustdesk library from Artifacts
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: librustdesk.so.armv7-linux-androideabi
|
||||
path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||
|
||||
- name: Download Rustdesk library from Artifacts
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: librustdesk.so.x86_64-linux-android
|
||||
path: ./flutter/android/app/src/main/jniLibs/x86_64
|
||||
|
||||
- name: Download Rustdesk library from Artifacts
|
||||
if: ${{ env.reltype == 'debug' }}
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: librustdesk.so.i686-linux-android
|
||||
path: ./flutter/android/app/src/main/jniLibs/x86
|
||||
|
||||
- name: fix android for flutter 3.13
|
||||
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||
run: |
|
||||
cd flutter
|
||||
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'
|
||||
|
||||
- name: Build rustdesk
|
||||
shell: bash
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
|
||||
run: |
|
||||
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
|
||||
# temporary use debug sign config
|
||||
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
|
||||
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
|
||||
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
|
||||
mv ./flutter/android/app/src/main/jniLibs/armeabi-v7a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
|
||||
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
|
||||
mv ./flutter/android/app/src/main/jniLibs/x86_64/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
|
||||
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/
|
||||
if [ "${{ env.reltype }}" = "debug" ]; then
|
||||
mv ./flutter/android/app/src/main/jniLibs/x86/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
|
||||
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/
|
||||
fi
|
||||
# build flutter
|
||||
pushd flutter
|
||||
flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64${{ env.x86_target }}
|
||||
popd
|
||||
mkdir -p signed-apk
|
||||
mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
|
||||
|
||||
- uses: r0adkll/sign-android-release@v1
|
||||
name: Sign app APK
|
||||
if: env.ANDROID_SIGNING_KEY != null
|
||||
id: sign-rustdesk
|
||||
with:
|
||||
releaseDirectory: ./signed-apk
|
||||
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
|
||||
alias: ${{ secrets.ANDROID_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
env:
|
||||
# override default build-tools version (29.0.3) -- optional
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
||||
|
||||
- name: Publish signed apk package
|
||||
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
||||
|
||||
- name: Publish unsigned apk package
|
||||
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
|
||||
|
||||
build-rustdesk-linux:
|
||||
needs: [generate-bridge-linux]
|
||||
name: build rustdesk linux ${{ matrix.job.target }}
|
||||
name: build rustdesk linux ${{ matrix.job.target }}
|
||||
runs-on: ${{ matrix.job.on }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1010,7 +1327,7 @@ jobs:
|
||||
sudo apt-get install -y nasm qemu-user-static
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Swap Space
|
||||
if: ${{ matrix.job.arch == 'x86_64' }}
|
||||
@@ -1054,11 +1371,24 @@ jobs:
|
||||
with:
|
||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||
if ! $VCPKG_ROOT/vcpkg \
|
||||
install \
|
||||
--triplet ${{ matrix.job.vcpkg-triplet }} \
|
||||
--x-install-root="$VCPKG_ROOT/installed"; then
|
||||
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
|
||||
echo "$_1:"
|
||||
echo "======"
|
||||
cat "$_1"
|
||||
echo "======"
|
||||
echo ""
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Restore bridge files
|
||||
@@ -1094,7 +1424,7 @@ jobs:
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libappindicator3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libclang-10-dev \
|
||||
libgstreamer1.0-dev \
|
||||
@@ -1237,16 +1567,16 @@ jobs:
|
||||
rustdesk-*.deb
|
||||
rustdesk-*.rpm
|
||||
|
||||
- name: Upload deb
|
||||
- name: Upload deb
|
||||
uses: actions/upload-artifact@master
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
with:
|
||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
||||
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
||||
|
||||
|
||||
# only x86_64 for arch since we can not find newest arm64 docker image to build
|
||||
# old arch image does not make sense for arch since it is "arch" which always update to date
|
||||
# and failed to makepkg arm64 on x86_64
|
||||
# and failed to makepkg arm64 on x86_64
|
||||
- name: Patch archlinux PKGBUILD
|
||||
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
|
||||
run: |
|
||||
@@ -1290,6 +1620,7 @@ jobs:
|
||||
deb_arch: amd64,
|
||||
sciter_arch: x64,
|
||||
vcpkg-triplet: x64-linux,
|
||||
extra_features: ",hwcodec",
|
||||
}
|
||||
- {
|
||||
arch: armv7,
|
||||
@@ -1299,6 +1630,7 @@ jobs:
|
||||
deb_arch: armhf,
|
||||
sciter_arch: arm32,
|
||||
vcpkg-triplet: arm-linux,
|
||||
extra_features: "",
|
||||
}
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
@@ -1308,25 +1640,8 @@ jobs:
|
||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
cmake \
|
||||
crossbuild-essential-armhf \
|
||||
curl \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
nasm \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
unzip \
|
||||
xz-utils \
|
||||
zip
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free Space
|
||||
run: |
|
||||
@@ -1336,7 +1651,7 @@ jobs:
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
toolchain: ${{ env.SCITER_RUST_VERSION }}
|
||||
targets: ${{ matrix.job.target }}
|
||||
components: "rustfmt"
|
||||
|
||||
@@ -1345,18 +1660,6 @@ jobs:
|
||||
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
|
||||
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake
|
||||
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||
shell: bash
|
||||
|
||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||
name: Build rustdesk sciter binary for ${{ matrix.job.arch }}
|
||||
id: vcpkg
|
||||
@@ -1368,19 +1671,17 @@ jobs:
|
||||
ls -l "${PWD}"
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}:/workspace"
|
||||
--volume "/opt/artifacts:/opt/artifacts"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt-get update -y
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
clang \
|
||||
cmake \
|
||||
curl \
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libappindicator3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libclang-dev \
|
||||
libdbus-1-dev \
|
||||
@@ -1398,39 +1699,93 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
python3 \
|
||||
python3.7 \
|
||||
rpm \
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
xz-utils \
|
||||
zip
|
||||
# arm-linux needs CMake and vcokg built from source as there
|
||||
# are no prebuilts available from Kitware and Microsoft
|
||||
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
|
||||
# install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake
|
||||
apt-get install -y gcc-8 g++-8 libssl-dev
|
||||
# bootstrap CMake amd add it to PATH
|
||||
git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake
|
||||
pushd /tmp/cmake
|
||||
./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf"
|
||||
make -j1 install
|
||||
popd
|
||||
rm -rf /tmp/cmake
|
||||
export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH"
|
||||
fi
|
||||
# bootstrap vcpkg and set VCPKG_ROOT
|
||||
export VCPKG_ROOT=/opt/artifacts/vcpkg
|
||||
mkdir -p /opt/artifacts
|
||||
pushd /opt/artifacts
|
||||
rm -rf vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
pushd vcpkg
|
||||
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
|
||||
# build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux
|
||||
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
|
||||
CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics
|
||||
else
|
||||
sh bootstrap-vcpkg.sh -disableMetrics
|
||||
fi
|
||||
popd
|
||||
popd
|
||||
# rust
|
||||
pushd /opt
|
||||
# do not use rustup, because memory overflow in qemu
|
||||
wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
|
||||
wget --output-document rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
|
||||
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
|
||||
cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh
|
||||
pushd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
|
||||
./install.sh
|
||||
popd
|
||||
rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
|
||||
# edit config
|
||||
popd
|
||||
# install newer nasm for aom
|
||||
wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_${{ env.SCITER_NASM_DEBVERSION }}_${{ matrix.job.deb_arch }}.deb"
|
||||
dpkg -i nasm.deb
|
||||
rm -f nasm.deb
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
# set python3.7 as default python3
|
||||
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1
|
||||
# add built CMake to PATH and set VCPKG_FORCE_SYSTEM_BINARIES Afor arm-linux
|
||||
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
|
||||
export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH"
|
||||
export VCPKG_FORCE_SYSTEM_BINARIES=1
|
||||
fi
|
||||
# edit cargo config
|
||||
mkdir -p ~/.cargo/
|
||||
echo """
|
||||
[source.crates-io]
|
||||
registry = 'https://github.com/rust-lang/crates.io-index'
|
||||
""" > ~/.cargo/config
|
||||
cat ~/.cargo/config
|
||||
|
||||
# build
|
||||
pushd /workspace
|
||||
python3 ./res/inline-sciter.py
|
||||
# install dependencies from vcpkg
|
||||
export VCPKG_ROOT=/opt/artifacts/vcpkg
|
||||
if ! $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"; then
|
||||
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
|
||||
echo "$_1:"
|
||||
echo "======"
|
||||
cat "$_1"
|
||||
echo "======"
|
||||
echo ""
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
# build rustdesk
|
||||
python3 ./res/inline-sciter.py
|
||||
export CARGO_INCREMENTAL=0
|
||||
cargo build --features inline --release --bins --jobs 1
|
||||
# package
|
||||
cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
|
||||
# make debian package
|
||||
mkdir -p ./Release
|
||||
mv ./target/release/rustdesk ./Release/rustdesk
|
||||
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so
|
||||
@@ -1454,7 +1809,7 @@ jobs:
|
||||
files: |
|
||||
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||
|
||||
- name: Upload deb
|
||||
- name: Upload deb
|
||||
uses: actions/upload-artifact@master
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
with:
|
||||
@@ -1470,17 +1825,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
arch: x86_64,
|
||||
}
|
||||
- {
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
arch: aarch64,
|
||||
}
|
||||
- { target: x86_64-unknown-linux-gnu, arch: x86_64 }
|
||||
- { target: aarch64-unknown-linux-gnu, arch: aarch64 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Binary
|
||||
uses: actions/download-artifact@master
|
||||
@@ -1519,7 +1868,7 @@ jobs:
|
||||
|
||||
build-flatpak:
|
||||
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
|
||||
needs:
|
||||
needs:
|
||||
- build-rustdesk-linux
|
||||
- build-rustdesk-linux-sciter
|
||||
runs-on: ${{ matrix.job.on }}
|
||||
@@ -1552,7 +1901,7 @@ jobs:
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Binary
|
||||
uses: actions/download-artifact@master
|
||||
@@ -1587,7 +1936,7 @@ jobs:
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
pushd /workspace
|
||||
# install
|
||||
# install
|
||||
apt-get update -y
|
||||
apt-get install -y \
|
||||
cmake \
|
||||
@@ -1628,7 +1977,7 @@ jobs:
|
||||
RELEASE_NAME: web-basic
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare env
|
||||
run: |
|
||||
|
||||
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 version: 2024.06.15
|
||||
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||
VERSION: "1.2.7"
|
||||
VERSION: "1.3.1"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
git \
|
||||
g++ \
|
||||
g++-multilib \
|
||||
libappindicator3-dev \
|
||||
libayatana-appindicator3-dev\
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-10-dev \
|
||||
|
||||
388
Cargo.lock
generated
388
Cargo.lock
generated
@@ -123,7 +123,7 @@ checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
[[package]]
|
||||
name = "android-wakelock"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/21pages/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90"
|
||||
source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90"
|
||||
dependencies = [
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.4.0"
|
||||
source = "git+https://github.com/rustdesk-org/arboard#27b4e503caa70ec6306e5270461429f2cf907ad6"
|
||||
source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"core-graphics 0.23.2",
|
||||
@@ -234,24 +234,13 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"resvg",
|
||||
"serde 1.0.203",
|
||||
"serde_derive",
|
||||
"windows-sys 0.48.0",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.5.1"
|
||||
@@ -871,6 +860,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
@@ -987,11 +982,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "clipboard-master"
|
||||
version = "4.0.0-beta.6"
|
||||
source = "git+https://github.com/rustdesk-org/clipboard-master#5268c7b3d7728699566ad863da0911f249706f8c"
|
||||
source = "git+https://github.com/rustdesk-org/clipboard-master#4fb62e5b62fb6350d82b571ec7ba94b3cd466695"
|
||||
dependencies = [
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-wlr",
|
||||
"windows-win",
|
||||
"wl-clipboard-rs",
|
||||
"x11-clipboard 0.9.2",
|
||||
@@ -1000,9 +998,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.3.1"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad"
|
||||
checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
@@ -1526,12 +1524,6 @@ dependencies = [
|
||||
"dasp_sample",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.7"
|
||||
@@ -1691,7 +1683,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.7.4",
|
||||
"libloading 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2081,12 +2073,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.0"
|
||||
@@ -2143,29 +2129,6 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontconfig-parser"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d"
|
||||
dependencies = [
|
||||
"roxmltree 0.19.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontdb"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
|
||||
dependencies = [
|
||||
"fontconfig-parser",
|
||||
"log",
|
||||
"memmap2",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -3087,8 +3050,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hwcodec"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/21pages/hwcodec#89879f2f02c6f74e88a4a43744a1153aec5b7e7f"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -3213,16 +3176,10 @@ dependencies = [
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
|
||||
|
||||
[[package]]
|
||||
name = "impersonate_system"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/21pages/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167"
|
||||
source = "git+https://github.com/rustdesk-org/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -3462,16 +3419,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -3558,7 +3505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3733,7 +3680,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "machine-uid"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/21pages/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542"
|
||||
source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -3777,15 +3724,6 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.6.5"
|
||||
@@ -3847,14 +3785,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mouce"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/rustdesk-org/mouce.git#177625a395cd8fa73964714d0039535cb9b47893"
|
||||
dependencies = [
|
||||
"glob",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.13.5"
|
||||
@@ -4043,11 +3973,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.1.1",
|
||||
"libc",
|
||||
"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]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -4732,15 +4674,9 @@ version = "0.7.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
|
||||
dependencies = [
|
||||
"siphasher 0.2.3",
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pico-args"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.5"
|
||||
@@ -5065,6 +5001,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.6.13"
|
||||
@@ -5260,7 +5205,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rdev"
|
||||
version = "0.5.0-2"
|
||||
source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488"
|
||||
source = "git+https://github.com/rustdesk-org/rdev#d4c1759926d693ba269e2cb8cf9f87b13e424e4e"
|
||||
dependencies = [
|
||||
"cocoa 0.24.1",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -5414,31 +5359,6 @@ dependencies = [
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051"
|
||||
dependencies = [
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"log",
|
||||
"pico-args",
|
||||
"rgb",
|
||||
"svgtypes",
|
||||
"tiny-skia",
|
||||
"usvg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
@@ -5463,18 +5383,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "2.1.0"
|
||||
@@ -5572,7 +5480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.2.7"
|
||||
version = "1.3.1"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -5604,6 +5512,7 @@ dependencies = [
|
||||
"flutter_rust_bridge",
|
||||
"fon",
|
||||
"fruitbasket",
|
||||
"gtk",
|
||||
"hbb_common",
|
||||
"hex",
|
||||
"hound",
|
||||
@@ -5618,7 +5527,7 @@ dependencies = [
|
||||
"libpulse-simple-binding",
|
||||
"mac_address",
|
||||
"magnum-opus",
|
||||
"mouce",
|
||||
"nix 0.29.0",
|
||||
"num_cpus",
|
||||
"objc",
|
||||
"objc_id",
|
||||
@@ -5650,6 +5559,7 @@ dependencies = [
|
||||
"system_shutdown",
|
||||
"tao",
|
||||
"tauri-winrt-notification",
|
||||
"termios",
|
||||
"totp-rs",
|
||||
"tray-icon",
|
||||
"url",
|
||||
@@ -5670,7 +5580,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.2.7"
|
||||
version = "1.3.1"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
@@ -5853,22 +5763,6 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
||||
|
||||
[[package]]
|
||||
name = "rustybuzz"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytemuck",
|
||||
"smallvec",
|
||||
"ttf-parser",
|
||||
"unicode-bidi-mirroring",
|
||||
"unicode-ccc",
|
||||
"unicode-properties",
|
||||
"unicode-script",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
@@ -5905,7 +5799,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sciter-rs"
|
||||
version = "0.5.57"
|
||||
source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#fab913b7c2e779b05c249b0c5de5a08759b2c15d"
|
||||
source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
@@ -6159,27 +6053,12 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "simplecss"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@@ -6189,15 +6068,6 @@ dependencies = [
|
||||
"autocfg 1.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slotmap"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
@@ -6268,15 +6138,6 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
dependencies = [
|
||||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
@@ -6338,16 +6199,6 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "svgtypes"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"siphasher 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.15.44"
|
||||
@@ -6399,7 +6250,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.29.10"
|
||||
source = "git+https://github.com/rustdesk-org/sysinfo#f45dcc6510d48c3a1401c5a33eedccc8899f67b2"
|
||||
source = "git+https://github.com/rustdesk-org/sysinfo?branch=rlim_max#90b1705d909a4902dbbbdea37ee64db17841077d"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -6593,11 +6444,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tfc"
|
||||
version = "0.6.1"
|
||||
source = "git+https://github.com/rustdesk-org/The-Fat-Controller#9dd86151525fd010dc93f6bc9b6aedd1a75cc342"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#de9c8ba480f166a9fc90aaa47bb0e84b443ea9c6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"core-graphics 0.22.3",
|
||||
"core-graphics 0.23.2",
|
||||
"unicode-segmentation",
|
||||
"winapi 0.3.9",
|
||||
"x11 2.19.0",
|
||||
@@ -6687,32 +6538,6 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if 1.0.0",
|
||||
"log",
|
||||
"png",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.1"
|
||||
@@ -7004,12 +6829,6 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
@@ -7082,18 +6901,6 @@ version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi-mirroring"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ccc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
@@ -7109,30 +6916,12 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-script"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-vo"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.13"
|
||||
@@ -7195,33 +6984,6 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"data-url",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
"imagesize",
|
||||
"kurbo",
|
||||
"log",
|
||||
"pico-args",
|
||||
"roxmltree 0.20.0",
|
||||
"rustybuzz",
|
||||
"simplecss",
|
||||
"siphasher 1.0.1",
|
||||
"strict-num",
|
||||
"svgtypes",
|
||||
"tiny-skia-path",
|
||||
"unicode-bidi",
|
||||
"unicode-script",
|
||||
"unicode-vo",
|
||||
"xmlwriter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf16string"
|
||||
version = "0.2.0"
|
||||
@@ -7309,7 +7071,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "wallpaper"
|
||||
version = "3.2.0"
|
||||
source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
|
||||
source = "git+https://github.com/rustdesk-org/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"enquote",
|
||||
@@ -7414,9 +7176,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.4"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07"
|
||||
checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
@@ -7428,9 +7190,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.3"
|
||||
version = "0.31.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133"
|
||||
checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"rustix 0.38.34",
|
||||
@@ -7440,9 +7202,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.1"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83d0f1056570486e26a3773ec633885124d79ae03827de05ba6c85f79904026c"
|
||||
checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"wayland-backend",
|
||||
@@ -7452,9 +7214,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-wlr"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7dab47671043d9f5397035975fe1cac639e5bca5cc0b3c32d09f01612e34d24"
|
||||
checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"wayland-backend",
|
||||
@@ -7465,20 +7227,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.2"
|
||||
version = "0.31.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565"
|
||||
checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.86",
|
||||
"quick-xml 0.31.0",
|
||||
"quick-xml 0.34.0",
|
||||
"quote 1.0.36",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.2"
|
||||
version = "0.31.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12"
|
||||
checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
@@ -7498,7 +7260,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webm"
|
||||
version = "1.1.0"
|
||||
source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
|
||||
source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
|
||||
dependencies = [
|
||||
"webm-sys",
|
||||
]
|
||||
@@ -7506,7 +7268,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "webm-sys"
|
||||
version = "1.0.4"
|
||||
source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
|
||||
source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -8220,12 +7982,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xmlwriter"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "3.15.2"
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.2.7"
|
||||
version = "1.3.1"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -90,7 +90,7 @@ enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||
clipboard = { path = "libs/clipboard" }
|
||||
ctrlc = "3.2"
|
||||
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
|
||||
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] }
|
||||
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
|
||||
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||
|
||||
system_shutdown = "4.0"
|
||||
@@ -114,7 +114,7 @@ winapi = { version = "0.3", features = [
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
virtual_display = { path = "libs/virtual_display" }
|
||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
||||
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
|
||||
shared_memory = "0.12"
|
||||
tauri-winrt-notification = "0.1.2"
|
||||
runas = "1.2"
|
||||
@@ -138,7 +138,7 @@ image = "0.24"
|
||||
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
|
||||
wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
@@ -152,7 +152,6 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
||||
async-process = "1.7"
|
||||
mouce = { git="https://github.com/rustdesk-org/mouce.git" }
|
||||
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
@@ -162,11 +161,14 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
percent-encoding = {version = "2.3", optional = true}
|
||||
once_cell = {version = "1.18", optional = true}
|
||||
nix = { version = "0.29", features = ["term", "process"]}
|
||||
gtk = "0.18"
|
||||
termios = "0.3"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
jni = "0.21"
|
||||
android-wakelock = { git = "https://github.com/21pages/android-wakelock" }
|
||||
android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
|
||||
|
||||
[workspace]
|
||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
||||
|
||||
12
README.md
12
README.md
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#free-public-servers">Servers</a> •
|
||||
<a href="#public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
@@ -59,19 +59,19 @@ Please download Sciter dynamic library yourself.
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
@@ -171,3 +171,7 @@ Please ensure that you are running these commands from the root of the RustDesk
|
||||

|
||||
|
||||

|
||||
|
||||
## [Public Servers](#public-servers)
|
||||
|
||||
RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.2.7
|
||||
version: 1.3.1
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.2.7
|
||||
version: 1.3.1
|
||||
exec: usr/lib/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
@@ -37,6 +37,9 @@ AppDir:
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
||||
universe multiverse
|
||||
include:
|
||||
# https://github.com/rustdesk/rustdesk/issues/9103
|
||||
# Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try.
|
||||
# We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it.
|
||||
- libc6:amd64
|
||||
- libgtk-3-0
|
||||
- libxcb-randr0
|
||||
|
||||
14
build.py
14
build.py
@@ -31,6 +31,11 @@ def get_deb_arch() -> str:
|
||||
return "amd64"
|
||||
return custom_arch
|
||||
|
||||
def get_deb_extra_depends() -> str:
|
||||
custom_arch = os.environ.get("DEB_ARCH")
|
||||
if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so
|
||||
return ", libatomic1"
|
||||
return ""
|
||||
|
||||
def system2(cmd):
|
||||
exit_code = os.system(cmd)
|
||||
@@ -282,10 +287,11 @@ Version: %s
|
||||
Architecture: %s
|
||||
Maintainer: rustdesk <info@rustdesk.com>
|
||||
Homepage: https://rustdesk.com
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
||||
Recommends: libayatana-appindicator3-1
|
||||
Description: A remote control software.
|
||||
|
||||
""" % (version, get_deb_arch())
|
||||
""" % (version, get_deb_arch(), get_deb_extra_depends())
|
||||
file = open(control_file_path, "w")
|
||||
file.write(content)
|
||||
file.close()
|
||||
@@ -325,8 +331,6 @@ def build_flutter_deb(version, features):
|
||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||
system2(
|
||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||
system2(
|
||||
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
|
||||
system2(
|
||||
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||
system2(
|
||||
@@ -370,8 +374,6 @@ def build_deb_from_folder(version, binary_folder):
|
||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||
system2(
|
||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||
system2(
|
||||
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
|
||||
system2(
|
||||
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
||||
|
||||
|
||||
87
docs/CODE_OF_CONDUCT-ZH.md
Normal file
87
docs/CODE_OF_CONDUCT-ZH.md
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
# 贡献者公约行为准则
|
||||
|
||||
## 我们的承诺
|
||||
|
||||
身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。
|
||||
|
||||
我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。
|
||||
|
||||
## 我们的标准
|
||||
|
||||
有助于为我们的社区创造积极环境的行为例子包括但不限于:
|
||||
|
||||
* 表现出对他人的同情和善意
|
||||
* 尊重不同的主张、观点和感受
|
||||
* 提出和大方接受建设性意见
|
||||
* 承担责任并向受我们错误影响的人道歉
|
||||
* 注重社区共同诉求,而非个人得失
|
||||
|
||||
不当行为例子包括:
|
||||
|
||||
* 使用情色化的语言或图像,及性引诱或挑逗
|
||||
* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击
|
||||
* 公开或私下的骚扰行为
|
||||
* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址
|
||||
* 其他有理由认定为违反职业操守的不当行为
|
||||
|
||||
## 责任和权力
|
||||
|
||||
社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。
|
||||
|
||||
社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论(comment)、提交(commits)、代码、维基(wiki)编辑、议题(issues)或其他贡献,并在适当时机知采取措施的理由。
|
||||
|
||||
## 适用范围
|
||||
|
||||
本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。
|
||||
|
||||
代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。
|
||||
|
||||
## 监督
|
||||
|
||||
辱骂、骚扰或其他不可接受的行为可通过[info@rustdesk.com](mailto:info@rustdesk.com)向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。
|
||||
|
||||
所有社区领袖都有义务尊重任何事件报告者的隐私和安全。
|
||||
|
||||
## 处理方针
|
||||
|
||||
社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式:
|
||||
|
||||
### 1. 纠正
|
||||
|
||||
**社区影响**: 使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。
|
||||
|
||||
**处理意见**: 由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。
|
||||
|
||||
### 2. 警告
|
||||
|
||||
**社区影响**: 单个或一系列违规行为。
|
||||
|
||||
**处理意见**: 警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。
|
||||
|
||||
### 3. 临时封禁
|
||||
|
||||
**社区影响**: 严重违反社区准则,包括持续的不当行为。
|
||||
|
||||
**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。
|
||||
|
||||
### 4. 永久封禁
|
||||
|
||||
**社区影响**: 行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。
|
||||
|
||||
**处理意见**: 永久禁止在社区内进行任何形式的公开互动。
|
||||
|
||||
## 参见
|
||||
|
||||
本行为准则改编自[参与者公约][homepage]2.0 版, 参见
|
||||
[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
指导方针借鉴自[Mozilla纪检分级][Mozilla CoC].
|
||||
|
||||
有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译参见[https://www.contributor-covenant.org/translations][translations]。
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
32
docs/CONTRIBUTING-ZH.md
Normal file
32
docs/CONTRIBUTING-ZH.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 为RustDesk做贡献
|
||||
|
||||
Rust欢迎每一位贡献者,如果您有意向为我们做出贡献,请遵循以下指南:
|
||||
|
||||
## 贡献方式
|
||||
|
||||
对 RustDesk 或其依赖项的贡献需要通过 GitHub 的 Pull Request (PR) 的形式提交。每个 PR 都会由核心贡献者(即有权限合并代码的人)进行审核,审核通过后代码会合并到主分支,或者您会收到需要修改的反馈。所有贡献者,包括核心贡献者,提交的代码都应遵循此流程。
|
||||
|
||||
如果您希望处理某个问题,请先在对应的 GitHub issue 下发表评论,声明您将处理该问题,以避免该问题被多位贡献者重复处理。
|
||||
|
||||
## PR 注意事项
|
||||
|
||||
- 从 master 分支创建一个新的分支,并在提交PR之前,如果需要,将您的分支 变基(rebase) 到最新的 master 分支。如果您的分支无法顺利合并到 master 分支,您可能会被要求更新您的代码。
|
||||
|
||||
- 每次提交的改动应该尽可能少,并且要保证每次提交的代码都是正确的(即每个 commit 都应能成功编译并通过测试)。
|
||||
|
||||
- 每个提交都应附有开发者证书签名(http://developercertificate.org), 表明您(以及您的雇主,若适用)同意遵守项目[许可证条款](../LICENCE)。在使用 git 提交代码时,可以通过在 `git commit` 时使用 `-s` 选项加入签名
|
||||
|
||||
- 如果您的 PR 未被及时审核,或需要指定的人员进行审核,您可以通过在 PR 或评论中 @ 提到相关审核者,以及发送[电子邮件](mailto:info@rustdesk.com)的方式请求审核。
|
||||
|
||||
- 请为修复的 bug 或新增的功能添加相应的测试用例。
|
||||
|
||||
有关具体的 git 使用说明,请参考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||
|
||||
## 行为准则
|
||||
|
||||
请遵守项目的[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。
|
||||
|
||||
|
||||
## 沟通渠道
|
||||
|
||||
RustDesk 的贡献者主要通过 [Discord](https://discord.gg/nDceKgxnkV) 进行交流。
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -43,15 +43,13 @@ arm64-v8a)
|
||||
FLUTTER_TARGET=android-arm64
|
||||
NDK_TARGET=aarch64-linux-android
|
||||
RUST_TARGET=aarch64-linux-android
|
||||
# RUSTDESK_FEATURES='flutter,hwcodec'
|
||||
RUSTDESK_FEATURES='flutter'
|
||||
RUSTDESK_FEATURES='flutter,hwcodec'
|
||||
;;
|
||||
armeabi-v7a)
|
||||
FLUTTER_TARGET=android-arm
|
||||
NDK_TARGET=arm-linux-androideabi
|
||||
RUST_TARGET=armv7-linux-androideabi
|
||||
# RUSTDESK_FEATURES='flutter,hwcodec'
|
||||
RUSTDESK_FEATURES='flutter'
|
||||
RUSTDESK_FEATURES='flutter,hwcodec'
|
||||
;;
|
||||
x86_64)
|
||||
FLUTTER_TARGET=android-x64
|
||||
|
||||
@@ -31,7 +31,6 @@ import 'mobile/pages/file_manager_page.dart';
|
||||
import 'mobile/pages/remote_page.dart';
|
||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'models/input_model.dart';
|
||||
import 'models/model.dart';
|
||||
import 'models/platform_model.dart';
|
||||
|
||||
@@ -51,6 +50,9 @@ final isLinux = isLinux_;
|
||||
final isDesktop = isDesktop_;
|
||||
final isWeb = isWeb_;
|
||||
final isWebDesktop = isWebDesktop_;
|
||||
final isWebOnWindows = isWebOnWindows_;
|
||||
final isWebOnLinux = isWebOnLinux_;
|
||||
final isWebOnMacOs = isWebOnMacOS_;
|
||||
var isMobile = isAndroid || isIOS;
|
||||
var version = '';
|
||||
int androidVersion = 0;
|
||||
@@ -348,6 +350,9 @@ class MyTheme {
|
||||
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
dialogBackgroundColor: Colors.white,
|
||||
appBarTheme: AppBarTheme(
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
elevation: 15,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -443,6 +448,9 @@ class MyTheme {
|
||||
hoverColor: Color.fromARGB(255, 45, 46, 53),
|
||||
scaffoldBackgroundColor: Color(0xFF18191E),
|
||||
dialogBackgroundColor: Color(0xFF18191E),
|
||||
appBarTheme: AppBarTheme(
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
elevation: 15,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -548,7 +556,7 @@ class MyTheme {
|
||||
|
||||
static void changeDarkMode(ThemeMode mode) async {
|
||||
Get.changeThemeMode(mode);
|
||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
||||
if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
|
||||
if (mode == ThemeMode.system) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
||||
@@ -556,7 +564,7 @@ class MyTheme {
|
||||
await bind.mainSetLocalOption(
|
||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||
}
|
||||
await bind.mainChangeTheme(dark: mode.toShortString());
|
||||
if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
|
||||
// Synchronize the window theme of the system.
|
||||
updateSystemWindowTheme();
|
||||
}
|
||||
@@ -630,10 +638,30 @@ List<Locale> supportedLocales = const [
|
||||
Locale('da'),
|
||||
Locale('eo'),
|
||||
Locale('tr'),
|
||||
Locale('vi'),
|
||||
Locale('pl'),
|
||||
Locale('kz'),
|
||||
Locale('es'),
|
||||
Locale('nl'),
|
||||
Locale('nb'),
|
||||
Locale('et'),
|
||||
Locale('eu'),
|
||||
Locale('bg'),
|
||||
Locale('be'),
|
||||
Locale('vn'),
|
||||
Locale('uk'),
|
||||
Locale('fa'),
|
||||
Locale('ca'),
|
||||
Locale('el'),
|
||||
Locale('sv'),
|
||||
Locale('sq'),
|
||||
Locale('sr'),
|
||||
Locale('th'),
|
||||
Locale('sl'),
|
||||
Locale('ro'),
|
||||
Locale('lt'),
|
||||
Locale('lv'),
|
||||
Locale('ar'),
|
||||
Locale('he'),
|
||||
Locale('hr'),
|
||||
];
|
||||
|
||||
String formatDurationToTime(Duration duration) {
|
||||
@@ -647,8 +675,12 @@ String formatDurationToTime(Duration duration) {
|
||||
|
||||
closeConnection({String? id}) {
|
||||
if (isAndroid || isIOS) {
|
||||
gFFI.chatModel.hideChatOverlay();
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
() async {
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
gFFI.chatModel.hideChatOverlay();
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
}();
|
||||
} else {
|
||||
if (isWeb) {
|
||||
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||
@@ -1054,6 +1086,49 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget createDialogContent(String text) {
|
||||
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
|
||||
final List<TextSpan> spans = [];
|
||||
int start = 0;
|
||||
bool hasLink = false;
|
||||
|
||||
linkRegExp.allMatches(text).forEach((match) {
|
||||
hasLink = true;
|
||||
if (match.start > start) {
|
||||
spans.add(TextSpan(text: text.substring(start, match.start)));
|
||||
}
|
||||
spans.add(TextSpan(
|
||||
text: match.group(0) ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
String linkText = match.group(0) ?? '';
|
||||
linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
|
||||
launchUrl(Uri.parse(linkText));
|
||||
},
|
||||
));
|
||||
start = match.end;
|
||||
});
|
||||
|
||||
if (start < text.length) {
|
||||
spans.add(TextSpan(text: text.substring(start)));
|
||||
}
|
||||
|
||||
if (!hasLink) {
|
||||
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||
}
|
||||
|
||||
return SelectableText.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(color: Colors.black, fontSize: 15),
|
||||
children: spans,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void msgBox(SessionID sessionId, String type, String title, String text,
|
||||
String link, OverlayDialogManager dialogManager,
|
||||
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
|
||||
@@ -1211,7 +1286,7 @@ Widget msgboxContent(String type, String title, String text) {
|
||||
translate(title),
|
||||
style: TextStyle(fontSize: 21),
|
||||
).marginOnly(bottom: 10),
|
||||
Text(translateText(text), style: const TextStyle(fontSize: 15)),
|
||||
createDialogContent(translateText(text)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1406,14 +1481,10 @@ class AndroidPermissionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move this to mobile/widgets.
|
||||
// Used only for mobile, pages remote, settings, dialog
|
||||
// TODO remove argument contentPadding, it’s not used, getToggle() has not
|
||||
RadioListTile<T> getRadio<T>(
|
||||
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
|
||||
{EdgeInsetsGeometry? contentPadding, bool? dense}) {
|
||||
{bool? dense}) {
|
||||
return RadioListTile<T>(
|
||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
title: title,
|
||||
@@ -2814,7 +2885,7 @@ Widget buildErrorBanner(BuildContext context,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Tooltip(
|
||||
message: translate(err.value),
|
||||
child: Text(
|
||||
child: SelectableText(
|
||||
translate(err.value),
|
||||
),
|
||||
)).marginSymmetric(vertical: 2),
|
||||
@@ -2935,6 +3006,16 @@ openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
||||
final displays = i == kAllDisplayValue
|
||||
? List.generate(pi.displays.length, (index) => index)
|
||||
: [i];
|
||||
// Try clear image model before switching from all displays
|
||||
// 1. The remote side has multiple displays.
|
||||
// 2. Do not use texture render.
|
||||
// 3. Connect to Display 1.
|
||||
// 4. Switch to multi-displays `kAllDisplayValue`
|
||||
// 5. Switch to Display 2.
|
||||
// Then the remote page will display last picture of Display 1 at the beginning.
|
||||
if (pi.forceTextureRender && i != kAllDisplayValue) {
|
||||
ffi.imageModel.clearImage();
|
||||
}
|
||||
bind.sessionSwitchDisplay(
|
||||
isDesktop: isDesktop,
|
||||
sessionId: ffi.sessionId,
|
||||
@@ -2968,11 +3049,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||
}
|
||||
|
||||
setNewConnectWindowFrame(
|
||||
int windowId, String peerId, int? display, Rect? screenRect) async {
|
||||
setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
|
||||
int? display, Rect? screenRect) async {
|
||||
if (screenRect == null) {
|
||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||
windowId: windowId, display: display, peerId: peerId);
|
||||
// Do not restore window position to new connection if there's a pre-session.
|
||||
// https://github.com/rustdesk/rustdesk/discussions/8825
|
||||
if (preSessionCount == 0) {
|
||||
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||
windowId: windowId, display: display, peerId: peerId);
|
||||
}
|
||||
} else {
|
||||
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||
}
|
||||
@@ -3069,6 +3154,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
|
||||
|
||||
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
|
||||
String? text) {
|
||||
text = text?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
final sc = ServerConfig.decode(text);
|
||||
@@ -3316,6 +3402,42 @@ bool isInHomePage() {
|
||||
return controller.state.value.selected == 0;
|
||||
}
|
||||
|
||||
Widget _buildPresetPasswordWarning() {
|
||||
if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) !=
|
||||
'N') {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
color: Colors.yellow,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
child: Text(
|
||||
translate("Security Alert"),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize:
|
||||
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
translate("preset_password_warning"),
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
); // Show a warning message if the Future completed with true
|
||||
}
|
||||
|
||||
Widget buildPresetPasswordWarningMobile() {
|
||||
if (bind.isPresetPasswordMobileOnly()) {
|
||||
return _buildPresetPasswordWarning();
|
||||
} else {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildPresetPasswordWarning() {
|
||||
return FutureBuilder<bool>(
|
||||
future: bind.isPresetPassword(),
|
||||
@@ -3326,32 +3448,7 @@ Widget buildPresetPasswordWarning() {
|
||||
return Text(
|
||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||
} else if (snapshot.hasData && snapshot.data == true) {
|
||||
if (bind.mainGetBuildinOption(
|
||||
key: kOptionRemovePresetPasswordWarning) !=
|
||||
'N') {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
color: Colors.yellow,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
child: Text(
|
||||
translate("Security Alert"),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize:
|
||||
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
translate("preset_password_warning"),
|
||||
style: TextStyle(color: Colors.red),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
); // Show a warning message if the Future completed with true
|
||||
return _buildPresetPasswordWarning();
|
||||
} else {
|
||||
return SizedBox
|
||||
.shrink(); // Show nothing if the Future completed with false or null
|
||||
@@ -3405,7 +3502,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
|
||||
);
|
||||
}
|
||||
|
||||
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
|
||||
get windowResizeEdgeSize =>
|
||||
isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
|
||||
|
||||
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
|
||||
// See _linuxWindowResizable for more details.
|
||||
@@ -3423,7 +3521,12 @@ setResizable(bool resizable) {
|
||||
|
||||
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
||||
|
||||
final isCustomClient = bind.isCustomClient();
|
||||
bool? _isCustomClient;
|
||||
bool get isCustomClient {
|
||||
_isCustomClient ??= bind.isCustomClient();
|
||||
return _isCustomClient!;
|
||||
}
|
||||
|
||||
get defaultOptionLang => isCustomClient ? 'default' : '';
|
||||
get defaultOptionTheme => isCustomClient ? 'system' : '';
|
||||
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
||||
@@ -3461,3 +3564,36 @@ disableWindowMovable(int? windowId) {
|
||||
WindowController.fromWindowId(windowId).setMovable(false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget netWorkErrorWidget() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(translate("network_error_tip")),
|
||||
ElevatedButton(
|
||||
onPressed: gFFI.userModel.refreshCurrentUser,
|
||||
child: Text(translate("Retry")))
|
||||
.marginSymmetric(vertical: 16),
|
||||
SelectableText(gFFI.userModel.networkError.value,
|
||||
style: TextStyle(fontSize: 11, color: Colors.red)),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
List<ResizeEdge>? get windowManagerEnableResizeEdges => isWindows
|
||||
? [
|
||||
ResizeEdge.topLeft,
|
||||
ResizeEdge.top,
|
||||
ResizeEdge.topRight,
|
||||
]
|
||||
: null;
|
||||
|
||||
List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
|
||||
? [
|
||||
SubWindowResizeEdge.topLeft,
|
||||
SubWindowResizeEdge.top,
|
||||
SubWindowResizeEdge.topRight,
|
||||
]
|
||||
: null;
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
import 'package:get/get.dart';
|
||||
@@ -35,17 +36,14 @@ class AddressBook extends StatefulWidget {
|
||||
class _AddressBookState extends State<AddressBook> {
|
||||
var menuPos = RelativeRect.fill;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
if (!gFFI.userModel.isLogin) {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||
return netWorkErrorWidget();
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -64,15 +62,16 @@ class _AddressBookState extends State<AddressBook> {
|
||||
retry: null, // remove retry
|
||||
close: () => gFFI.abModel.currentAbPushError.value = ''),
|
||||
Expanded(
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? _buildAddressBookDesktop()
|
||||
: _buildAddressBookMobile())
|
||||
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? _buildAddressBookPortrait()
|
||||
: _buildAddressBookLandscape()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Widget _buildAddressBookDesktop() {
|
||||
Widget _buildAddressBookLandscape() {
|
||||
return Row(
|
||||
children: [
|
||||
Offstage(
|
||||
@@ -109,7 +108,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddressBookMobile() {
|
||||
Widget _buildAddressBookPortrait() {
|
||||
const padding = 8.0;
|
||||
return Column(
|
||||
children: [
|
||||
@@ -242,14 +241,14 @@ class _AddressBookState extends State<AddressBook> {
|
||||
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||
}
|
||||
},
|
||||
customButton: Container(
|
||||
height: isDesktop ? 48 : 40,
|
||||
customButton: Obx(()=>Container(
|
||||
height: stateGlobal.isPortrait.isFalse ? 48 : 40,
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: buildItem(gFFI.abModel.currentName.value, button: true)),
|
||||
Icon(Icons.arrow_drop_down),
|
||||
]),
|
||||
),
|
||||
)),
|
||||
underline: Container(
|
||||
height: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
@@ -338,8 +337,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
showActionMenu: editPermission);
|
||||
}
|
||||
|
||||
final gridView = DynamicGridView.builder(
|
||||
shrinkWrap: isMobile,
|
||||
gridView(bool isPortrait) => DynamicGridView.builder(
|
||||
shrinkWrap: isPortrait,
|
||||
gridDelegate: SliverGridDelegateWithWrapping(),
|
||||
itemCount: tags.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
@@ -347,9 +346,9 @@ class _AddressBookState extends State<AddressBook> {
|
||||
return tagBuilder(e);
|
||||
});
|
||||
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return (isDesktop || isWebDesktop)
|
||||
? gridView
|
||||
: LimitedBox(maxHeight: maxHeight, child: gridView);
|
||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||
? gridView(false)
|
||||
: LimitedBox(maxHeight: maxHeight, child: gridView(true)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -425,7 +424,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
|
||||
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||
sortMenuItem(),
|
||||
if (gFFI.abModel.legacyMode.value)
|
||||
sortMenuItem(), // It's already sorted after pulling down
|
||||
if (canWrite) syncMenuItem(),
|
||||
filterMenuItem(),
|
||||
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||
@@ -508,9 +508,9 @@ class _AddressBookState extends State<AddressBook> {
|
||||
double marginBottom = 4;
|
||||
|
||||
row({required Widget lable, required Widget input}) {
|
||||
return Row(
|
||||
makeChild(bool isPortrait) => Row(
|
||||
children: [
|
||||
!isMobile
|
||||
!isPortrait
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: lable.marginOnly(right: 10))
|
||||
@@ -521,7 +521,8 @@ class _AddressBookState extends State<AddressBook> {
|
||||
child: input),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: !isMobile ? 8 : 0);
|
||||
).marginOnly(bottom: !isPortrait ? 8 : 0);
|
||||
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
@@ -544,24 +545,24 @@ class _AddressBookState extends State<AddressBook> {
|
||||
),
|
||||
],
|
||||
),
|
||||
input: TextField(
|
||||
input: Obx(() => TextField(
|
||||
controller: idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('ID'),
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('ID'),
|
||||
errorText: errorMsg,
|
||||
errorMaxLines: 5),
|
||||
)),
|
||||
))),
|
||||
row(
|
||||
lable: Text(
|
||||
translate('Alias'),
|
||||
style: style,
|
||||
),
|
||||
input: TextField(
|
||||
input: Obx(() => TextField(
|
||||
controller: aliasController,
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('Alias'),
|
||||
)),
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Alias'),
|
||||
),)),
|
||||
),
|
||||
if (isCurrentAbShared)
|
||||
row(
|
||||
@@ -569,11 +570,11 @@ class _AddressBookState extends State<AddressBook> {
|
||||
translate('Password'),
|
||||
style: style,
|
||||
),
|
||||
input: TextField(
|
||||
input: Obx(() => TextField(
|
||||
controller: passwordController,
|
||||
obscureText: !passwordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: !isMobile ? null : translate('Password'),
|
||||
labelText: stateGlobal.isPortrait.isFalse ? null : translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
passwordVisible
|
||||
@@ -587,7 +588,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
},
|
||||
),
|
||||
),
|
||||
)),
|
||||
),)),
|
||||
if (gFFI.abModel.currentAbTags.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
|
||||
@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
message: !(isDesktop || isWebDesktop)
|
||||
? ''
|
||||
: widget.peer.tags.isNotEmpty
|
||||
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'
|
||||
|
||||
@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
|
||||
String get name => translate('uppercase');
|
||||
@override
|
||||
bool validate(String value) {
|
||||
return value.contains(RegExp(r'[A-Z]'));
|
||||
return value.runes.any((int rune) {
|
||||
var character = String.fromCharCode(rune);
|
||||
return character.toUpperCase() == character &&
|
||||
character.toLowerCase() != character;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
|
||||
|
||||
@override
|
||||
bool validate(String value) {
|
||||
return value.contains(RegExp(r'[a-z]'));
|
||||
return value.runes.any((int rune) {
|
||||
var character = String.fromCharCode(rune);
|
||||
return character.toLowerCase() == character &&
|
||||
character.toUpperCase() != character;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
@@ -380,6 +381,7 @@ class DialogTextField extends StatelessWidget {
|
||||
final FocusNode? focusNode;
|
||||
final TextInputType? keyboardType;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final int? maxLength;
|
||||
|
||||
static const kUsernameTitle = 'Username';
|
||||
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
||||
@@ -397,6 +399,7 @@ class DialogTextField extends StatelessWidget {
|
||||
this.hintText,
|
||||
this.keyboardType,
|
||||
this.inputFormatters,
|
||||
this.maxLength,
|
||||
required this.title,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
@@ -423,6 +426,7 @@ class DialogTextField extends StatelessWidget {
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -679,6 +683,8 @@ class PasswordWidget extends StatefulWidget {
|
||||
this.reRequestFocus = false,
|
||||
this.hintText,
|
||||
this.errorText,
|
||||
this.title,
|
||||
this.maxLength,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
@@ -686,6 +692,8 @@ class PasswordWidget extends StatefulWidget {
|
||||
final bool reRequestFocus;
|
||||
final String? hintText;
|
||||
final String? errorText;
|
||||
final String? title;
|
||||
final int? maxLength;
|
||||
|
||||
@override
|
||||
State<PasswordWidget> createState() => _PasswordWidgetState();
|
||||
@@ -729,7 +737,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DialogTextField(
|
||||
title: translate(DialogTextField.kPasswordTitle),
|
||||
title: translate(widget.title ?? DialogTextField.kPasswordTitle),
|
||||
hintText: translate(widget.hintText ?? 'Enter your password'),
|
||||
controller: widget.controller,
|
||||
prefixIcon: DialogTextField.kPasswordIcon,
|
||||
@@ -748,6 +756,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
obscureText: !_passwordVisible,
|
||||
errorText: widget.errorText,
|
||||
focusNode: _focusNode,
|
||||
maxLength: widget.maxLength,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1121,7 +1130,7 @@ void showRequestElevationDialog(
|
||||
errorText: errPwd.isEmpty ? null : errPwd.value,
|
||||
),
|
||||
],
|
||||
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0),
|
||||
).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
|
||||
).marginOnly(top: 10),
|
||||
],
|
||||
),
|
||||
@@ -1829,6 +1838,7 @@ void changeBot({Function()? callback}) async {
|
||||
void change2fa({Function()? callback}) async {
|
||||
if (bind.mainHasValid2FaSync()) {
|
||||
await bind.mainSetOption(key: "2fa", value: "");
|
||||
await bind.mainClearTrustedDevices();
|
||||
callback?.call();
|
||||
return;
|
||||
}
|
||||
@@ -1896,6 +1906,7 @@ void enter2FaDialog(
|
||||
SessionID sessionId, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
final RxBool submitReady = false.obs;
|
||||
final RxBool trustThisDevice = false.obs;
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close, context) {
|
||||
@@ -1905,7 +1916,7 @@ void enter2FaDialog(
|
||||
}
|
||||
|
||||
submit() {
|
||||
gFFI.send2FA(sessionId, controller.text.trim());
|
||||
gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
@@ -1919,9 +1930,27 @@ void enter2FaDialog(
|
||||
onChanged: () => submitReady.value = codeField.isReady,
|
||||
);
|
||||
|
||||
final trustField = Obx(() => CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(translate("Trust this device")),
|
||||
value: trustThisDevice.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
trustThisDevice.value = value;
|
||||
},
|
||||
));
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('enter-2fa-title')),
|
||||
content: codeField,
|
||||
content: Column(
|
||||
children: [
|
||||
codeField,
|
||||
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
|
||||
trustField,
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton('Cancel',
|
||||
onPressed: cancel,
|
||||
@@ -2216,3 +2245,255 @@ void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void changeUnlockPinDialog(String oldPin, Function() callback) {
|
||||
final pinController = TextEditingController(text: oldPin);
|
||||
final confirmController = TextEditingController(text: oldPin);
|
||||
String? pinErrorText;
|
||||
String? confirmationErrorText;
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
pinErrorText = null;
|
||||
confirmationErrorText = null;
|
||||
final pin = pinController.text.trim();
|
||||
final confirm = confirmController.text.trim();
|
||||
if (pin != confirm) {
|
||||
setState(() {
|
||||
confirmationErrorText =
|
||||
translate('The confirmation is not identical.');
|
||||
});
|
||||
return;
|
||||
}
|
||||
final errorMsg = bind.mainSetUnlockPin(pin: pin);
|
||||
if (errorMsg != '') {
|
||||
setState(() {
|
||||
pinErrorText = translate(errorMsg);
|
||||
});
|
||||
return;
|
||||
}
|
||||
callback.call();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set PIN")),
|
||||
content: Column(
|
||||
children: [
|
||||
DialogTextField(
|
||||
title: 'PIN',
|
||||
controller: pinController,
|
||||
obscureText: true,
|
||||
errorText: pinErrorText,
|
||||
maxLength: maxLength,
|
||||
),
|
||||
DialogTextField(
|
||||
title: translate('Confirmation'),
|
||||
controller: confirmController,
|
||||
obscureText: true,
|
||||
errorText: confirmationErrorText,
|
||||
maxLength: maxLength,
|
||||
)
|
||||
],
|
||||
).marginOnly(bottom: 12),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void checkUnlockPinDialog(String correctPin, Function() passCallback) {
|
||||
final controller = TextEditingController();
|
||||
String? errorText;
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
final pin = controller.text.trim();
|
||||
if (correctPin != pin) {
|
||||
setState(() {
|
||||
errorText = translate('Wrong PIN');
|
||||
});
|
||||
return;
|
||||
}
|
||||
passCallback.call();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PasswordWidget(
|
||||
title: 'PIN',
|
||||
controller: controller,
|
||||
errorText: errorText,
|
||||
hintText: '',
|
||||
))
|
||||
],
|
||||
).marginOnly(bottom: 12),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void confrimDeleteTrustedDevicesDialog(
|
||||
RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
|
||||
CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
|
||||
() async {
|
||||
if (selectedDevices.isEmpty) return;
|
||||
if (selectedDevices.length == trustedDevices.length) {
|
||||
await bind.mainClearTrustedDevices();
|
||||
trustedDevices.clear();
|
||||
selectedDevices.clear();
|
||||
} else {
|
||||
final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
|
||||
await bind.mainRemoveTrustedDevices(json: json);
|
||||
trustedDevices.removeWhere((element) {
|
||||
return selectedDevices.contains(element.hwid);
|
||||
});
|
||||
selectedDevices.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void manageTrustedDeviceDialog() async {
|
||||
RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
|
||||
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Manage trusted devices")),
|
||||
content: trustedDevicesTable(trustedDevices, selectedDevices),
|
||||
actions: [
|
||||
Obx(() => dialogButton(translate("Delete"),
|
||||
onPressed: selectedDevices.isEmpty
|
||||
? null
|
||||
: () {
|
||||
confrimDeleteTrustedDevicesDialog(
|
||||
trustedDevices,
|
||||
selectedDevices,
|
||||
);
|
||||
},
|
||||
isOutline: false)
|
||||
.marginOnly(top: 12)),
|
||||
dialogButton(translate("Close"), onPressed: close, isOutline: true)
|
||||
.marginOnly(top: 12),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class TrustedDevice {
|
||||
late final Uint8List hwid;
|
||||
late final int time;
|
||||
late final String id;
|
||||
late final String name;
|
||||
late final String platform;
|
||||
|
||||
TrustedDevice.fromJson(Map<String, dynamic> json) {
|
||||
final hwidList = json['hwid'] as List<dynamic>;
|
||||
hwid = Uint8List.fromList(hwidList.cast<int>());
|
||||
time = json['time'];
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
platform = json['platform'];
|
||||
}
|
||||
|
||||
String daysRemaining() {
|
||||
final expiry = time + 90 * 24 * 60 * 60 * 1000;
|
||||
final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
|
||||
if (remaining < 0) {
|
||||
return '0';
|
||||
}
|
||||
return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
|
||||
}
|
||||
|
||||
static Future<List<TrustedDevice>> get() async {
|
||||
final List<TrustedDevice> devices = List.empty(growable: true);
|
||||
try {
|
||||
final devicesJson = await bind.mainGetTrustedDevices();
|
||||
if (devicesJson.isNotEmpty) {
|
||||
final devicesList = json.decode(devicesJson);
|
||||
if (devicesList is List) {
|
||||
for (var device in devicesList) {
|
||||
devices.add(TrustedDevice.fromJson(device));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
devices.sort((a, b) => b.time.compareTo(a.time));
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
|
||||
Widget trustedDevicesTable(
|
||||
RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
|
||||
RxBool selectAll = false.obs;
|
||||
setSelectAll() {
|
||||
if (selectedDevices.isNotEmpty &&
|
||||
selectedDevices.length == devices.length) {
|
||||
selectAll.value = true;
|
||||
} else {
|
||||
selectAll.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
devices.listen((_) {
|
||||
setSelectAll();
|
||||
});
|
||||
selectedDevices.listen((_) {
|
||||
setSelectAll();
|
||||
});
|
||||
return FittedBox(
|
||||
child: Obx(() => DataTable(
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Checkbox(
|
||||
value: selectAll.value,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
selectedDevices.clear();
|
||||
selectedDevices.addAll(devices.map((e) => e.hwid));
|
||||
} else {
|
||||
selectedDevices.clear();
|
||||
}
|
||||
},
|
||||
)),
|
||||
DataColumn(label: Text(translate('Platform'))),
|
||||
DataColumn(label: Text(translate('ID'))),
|
||||
DataColumn(label: Text(translate('Username'))),
|
||||
DataColumn(label: Text(translate('Days remaining'))),
|
||||
],
|
||||
rows: devices.map((device) {
|
||||
return DataRow(cells: [
|
||||
DataCell(Checkbox(
|
||||
value: selectedDevices.contains(device.hwid),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (value) {
|
||||
selectedDevices.remove(device.hwid);
|
||||
selectedDevices.add(device.hwid);
|
||||
} else {
|
||||
selectedDevices.remove(device.hwid);
|
||||
}
|
||||
},
|
||||
)),
|
||||
DataCell(Text(device.platform)),
|
||||
DataCell(Text(device.id)),
|
||||
DataCell(Text(device.name)),
|
||||
DataCell(Text(device.daysRemaining())),
|
||||
]);
|
||||
}).toList(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
|
||||
String _failedMsg = '';
|
||||
String _url = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||
import 'package:flutter_hbb/common/widgets/login.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
@@ -23,11 +24,6 @@ class _MyGroupState extends State<MyGroup> {
|
||||
RxString get searchUserText => gFFI.groupModel.searchUserText;
|
||||
static TextEditingController searchUserController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
@@ -35,6 +31,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: loginDialog, child: Text(translate("Login"))));
|
||||
} else if (gFFI.userModel.networkError.isNotEmpty) {
|
||||
return netWorkErrorWidget();
|
||||
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
@@ -48,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
|
||||
retry: null,
|
||||
close: () => gFFI.groupModel.groupLoadError.value = ''),
|
||||
Expanded(
|
||||
child: (isDesktop || isWebDesktop)
|
||||
? _buildDesktop()
|
||||
: _buildMobile())
|
||||
child: Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? _buildPortrait()
|
||||
: _buildLandscape())),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDesktop() {
|
||||
Widget _buildLandscape() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
@@ -92,7 +90,7 @@ class _MyGroupState extends State<MyGroup> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobile() {
|
||||
Widget _buildPortrait() {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -162,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
final listView = ListView.builder(
|
||||
shrinkWrap: isMobile,
|
||||
listView(bool isPortrait) => ListView.builder(
|
||||
shrinkWrap: isPortrait,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return (isDesktop || isWebDesktop)
|
||||
? listView
|
||||
: LimitedBox(maxHeight: maxHeight, child: listView);
|
||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||
? listView(false)
|
||||
: LimitedBox(maxHeight: maxHeight, child: listView(true)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ class Draggable extends StatefulWidget {
|
||||
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DraggableState();
|
||||
State<StatefulWidget> createState() => _DraggableState(chatModel);
|
||||
}
|
||||
|
||||
class _DraggableState extends State<Draggable> {
|
||||
@@ -362,10 +362,8 @@ class _DraggableState extends State<Draggable> {
|
||||
double _saveHeight = 0;
|
||||
double _lastBottomHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_chatModel = widget.chatModel;
|
||||
_DraggableState(ChatModel? chatModel) {
|
||||
_chatModel = chatModel;
|
||||
}
|
||||
|
||||
get position => widget.position.pos;
|
||||
@@ -467,7 +465,8 @@ class IOSDraggable extends StatefulWidget {
|
||||
final Widget Function(BuildContext) builder;
|
||||
|
||||
@override
|
||||
IOSDraggableState createState() => IOSDraggableState();
|
||||
IOSDraggableState createState() =>
|
||||
IOSDraggableState(chatModel, width, height);
|
||||
}
|
||||
|
||||
class IOSDraggableState extends State<IOSDraggable> {
|
||||
@@ -478,12 +477,10 @@ class IOSDraggableState extends State<IOSDraggable> {
|
||||
double _saveHeight = 0;
|
||||
double _lastBottomHeight = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_chatModel = widget.chatModel;
|
||||
_width = widget.width;
|
||||
_height = widget.height;
|
||||
IOSDraggableState(ChatModel? chatModel, double w, double h) {
|
||||
_chatModel = chatModel;
|
||||
_width = w;
|
||||
_height = h;
|
||||
}
|
||||
|
||||
DraggableKeyPosition get position => widget.position;
|
||||
@@ -598,7 +595,8 @@ class QualityMonitor extends StatelessWidget {
|
||||
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
|
||||
_row(
|
||||
"Codec", qualityMonitorModel.data.codecFormat ?? '-'),
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
if (!isWeb)
|
||||
_row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -53,14 +54,11 @@ class _PeerCardState extends State<_PeerCard>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
if (isDesktop || isWebDesktop) {
|
||||
return _buildDesktop();
|
||||
} else {
|
||||
return _buildMobile();
|
||||
}
|
||||
return Obx(() =>
|
||||
stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
|
||||
}
|
||||
|
||||
Widget _buildMobile() {
|
||||
Widget _buildPortrait() {
|
||||
final peer = super.widget.peer;
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
return Card(
|
||||
@@ -87,7 +85,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildDesktop() {
|
||||
Widget _buildLandscape() {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final peer = super.widget.peer;
|
||||
var deco = Rx<BoxDecoration?>(
|
||||
@@ -140,13 +138,13 @@ class _PeerCardState extends State<_PeerCard>
|
||||
final greyStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||
final child = Row(
|
||||
makeChild(bool isPortrait) => Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: str2color('${peer.id}${peer.platform}', 0x7f),
|
||||
borderRadius: isMobile
|
||||
borderRadius: isPortrait
|
||||
? BorderRadius.circular(_tileRadius)
|
||||
: BorderRadius.only(
|
||||
topLeft: Radius.circular(_tileRadius),
|
||||
@@ -154,11 +152,11 @@ class _PeerCardState extends State<_PeerCard>
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
width: isMobile ? 50 : 42,
|
||||
height: isMobile ? 50 : null,
|
||||
width: isPortrait ? 50 : 42,
|
||||
height: isPortrait ? 50 : null,
|
||||
child: Stack(
|
||||
children: [
|
||||
getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
|
||||
getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
|
||||
.paddingAll(6),
|
||||
if (_shouldBuildPasswordIcon(peer))
|
||||
Positioned(
|
||||
@@ -183,19 +181,19 @@ class _PeerCardState extends State<_PeerCard>
|
||||
child: Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
getOnline(isMobile ? 4 : 8, peer.online),
|
||||
getOnline(isPortrait ? 4 : 8, peer.online),
|
||||
Expanded(
|
||||
child: Text(
|
||||
peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
)),
|
||||
]).marginOnly(top: isMobile ? 0 : 2),
|
||||
]).marginOnly(top: isPortrait ? 0 : 2),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: isMobile ? null : greyStyle,
|
||||
style: isPortrait ? null : greyStyle,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -203,9 +201,9 @@ class _PeerCardState extends State<_PeerCard>
|
||||
],
|
||||
).marginOnly(top: 2),
|
||||
),
|
||||
isMobile
|
||||
? checkBoxOrActionMoreMobile(peer)
|
||||
: checkBoxOrActionMoreDesktop(peer, isTile: true),
|
||||
isPortrait
|
||||
? checkBoxOrActionMorePortrait(peer)
|
||||
: checkBoxOrActionMoreLandscape(peer, isTile: true),
|
||||
],
|
||||
).paddingOnly(left: 10.0, top: 3.0),
|
||||
),
|
||||
@@ -216,28 +214,27 @@ class _PeerCardState extends State<_PeerCard>
|
||||
.map((e) => gFFI.abModel.getCurrentAbTagColor(e))
|
||||
.toList();
|
||||
return Tooltip(
|
||||
message: isMobile
|
||||
message: !(isDesktop || isWebDesktop)
|
||||
? ''
|
||||
: peer.tags.isNotEmpty
|
||||
? '${translate('Tags')}: ${peer.tags.join(', ')}'
|
||||
: '',
|
||||
child: Stack(children: [
|
||||
deco == null
|
||||
? child
|
||||
: Obx(
|
||||
() => Container(
|
||||
Obx(() => deco == null
|
||||
? makeChild(stateGlobal.isPortrait.isTrue)
|
||||
: Container(
|
||||
foregroundDecoration: deco.value,
|
||||
child: child,
|
||||
child: makeChild(stateGlobal.isPortrait.isTrue),
|
||||
),
|
||||
),
|
||||
if (colors.isNotEmpty)
|
||||
Positioned(
|
||||
Obx(()=> Positioned(
|
||||
top: 2,
|
||||
right: isMobile ? 20 : 10,
|
||||
right: stateGlobal.isPortrait.isTrue ? 20 : 10,
|
||||
child: CustomPaint(
|
||||
painter: TagPainter(radius: 3, colors: colors),
|
||||
),
|
||||
)
|
||||
))
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -253,6 +250,9 @@ class _PeerCardState extends State<_PeerCard>
|
||||
color: Colors.transparent,
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
// to-do: memory leak here, more investigation needed.
|
||||
// Continious rebuilds of `Obx()` will cause memory leak here.
|
||||
// The simple demo does not have this issue.
|
||||
child: Obx(
|
||||
() => Container(
|
||||
foregroundDecoration: deco.value,
|
||||
@@ -316,7 +316,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
)),
|
||||
]).paddingSymmetric(vertical: 8)),
|
||||
checkBoxOrActionMoreDesktop(peer, isTile: false),
|
||||
checkBoxOrActionMoreLandscape(peer, isTile: false),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12.0),
|
||||
)
|
||||
@@ -362,7 +362,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
}
|
||||
}
|
||||
|
||||
Widget checkBoxOrActionMoreMobile(Peer peer) {
|
||||
Widget checkBoxOrActionMorePortrait(Peer peer) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
@@ -390,7 +390,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
}
|
||||
}
|
||||
|
||||
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) {
|
||||
Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
|
||||
final PeerTabModel peerTabModel = Provider.of(context);
|
||||
final selected = peerTabModel.isPeerSelected(peer.id);
|
||||
if (peerTabModel.multiSelectionMode) {
|
||||
@@ -636,8 +636,8 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
|
||||
@protected
|
||||
Future<bool> _isForceAlwaysRelay(String id) async {
|
||||
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))
|
||||
.isNotEmpty;
|
||||
return option2bool(kOptionForceAlwaysRelay,
|
||||
(await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -1203,6 +1203,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
}
|
||||
|
||||
void _rdpDialog(String id) async {
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
|
||||
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
|
||||
final portController = TextEditingController(text: port);
|
||||
@@ -1257,9 +1258,9 @@ void _rdpDialog(String id) async {
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: isDesktop ? 8 : 0),
|
||||
Row(
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
(isDesktop || isWebDesktop)
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
@@ -1270,17 +1271,17 @@ void _rdpDialog(String id) async {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: (isDesktop || isWebDesktop)
|
||||
labelText: isDesktop
|
||||
? null
|
||||
: translate('Username')),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
|
||||
Row(
|
||||
).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
(isDesktop || isWebDesktop)
|
||||
stateGlobal.isPortrait.isFalse
|
||||
? ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 140),
|
||||
child: Text(
|
||||
@@ -1291,8 +1292,9 @@ void _rdpDialog(String id) async {
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
maxLength: maxLength,
|
||||
decoration: InputDecoration(
|
||||
labelText: (isDesktop || isWebDesktop)
|
||||
labelText: isDesktop
|
||||
? null
|
||||
: translate('Password'),
|
||||
suffixIcon: IconButton(
|
||||
@@ -1304,7 +1306,7 @@ void _rdpDialog(String id) async {
|
||||
)),
|
||||
),
|
||||
],
|
||||
)
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -76,8 +77,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
|
||||
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_PeerTabPageState() {
|
||||
_loadLocalOptions();
|
||||
}
|
||||
|
||||
void _loadLocalOptions() {
|
||||
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||
if (uiType != '') {
|
||||
peerCardUiType.value = int.parse(uiType) == 0
|
||||
@@ -87,8 +91,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
: PeerUiType.list;
|
||||
}
|
||||
hideAbTagsPanel.value =
|
||||
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
|
||||
super.initState();
|
||||
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
|
||||
}
|
||||
|
||||
Future<void> handleTabSelection(int tabIndex) async {
|
||||
@@ -105,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Widget build(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget selectionWrap(Widget widget) {
|
||||
return model.multiSelectionMode ? createMultiSelectionBar() : widget;
|
||||
return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
|
||||
}
|
||||
|
||||
return Column(
|
||||
textBaseline: TextBaseline.ideographic,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Container(
|
||||
padding: (isDesktop || isWebDesktop)
|
||||
? null
|
||||
: EdgeInsets.symmetric(horizontal: 2),
|
||||
child: selectionWrap(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
visibleContextMenuListener(_createSwitchBar(context))),
|
||||
if (isMobile)
|
||||
..._mobileRightActions(context)
|
||||
else
|
||||
..._desktopRightActions(context)
|
||||
],
|
||||
)),
|
||||
),
|
||||
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0),
|
||||
Obx(() => SizedBox(
|
||||
height: 32,
|
||||
child: Container(
|
||||
padding: stateGlobal.isPortrait.isTrue
|
||||
? EdgeInsets.symmetric(horizontal: 2)
|
||||
: null,
|
||||
child: selectionWrap(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: visibleContextMenuListener(
|
||||
_createSwitchBar(context))),
|
||||
if (stateGlobal.isPortrait.isTrue)
|
||||
..._portraitRightActions(context)
|
||||
else
|
||||
..._landscapeRightActions(context)
|
||||
],
|
||||
)),
|
||||
),
|
||||
).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
|
||||
_createPeersView(),
|
||||
],
|
||||
);
|
||||
@@ -297,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
}
|
||||
|
||||
Widget visibleContextMenuListener(Widget child) {
|
||||
if (isMobile) {
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
return GestureDetector(
|
||||
onLongPressDown: (e) {
|
||||
final x = e.globalPosition.dx;
|
||||
@@ -359,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
.toList());
|
||||
}
|
||||
|
||||
Widget createMultiSelectionBar() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget createMultiSelectionBar(PeerTabModel model) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -378,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
Row(
|
||||
children: [
|
||||
selectionCount(model.selectedPeers.length),
|
||||
selectAll(),
|
||||
selectAll(model),
|
||||
closeSelection(),
|
||||
],
|
||||
)
|
||||
@@ -454,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
showToast(translate('Successful'));
|
||||
},
|
||||
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -475,7 +477,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
model.setMultiSelectionMode(false);
|
||||
},
|
||||
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
|
||||
).marginOnly(left: isMobile ? 11 : 6),
|
||||
).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -498,7 +500,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
});
|
||||
},
|
||||
child: Icon(Icons.tag))
|
||||
.marginOnly(left: isMobile ? 11 : 6),
|
||||
.marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -509,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget selectAll() {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
Widget selectAll(PeerTabModel model) {
|
||||
return Offstage(
|
||||
offstage:
|
||||
model.selectedPeers.length >= model.currentTabCachedPeers.length,
|
||||
@@ -554,10 +555,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _desktopRightActions(BuildContext context) {
|
||||
List<Widget> _landscapeRightActions(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
return [
|
||||
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
|
||||
const PeerSearchBar().marginOnly(right: 13),
|
||||
_createRefresh(
|
||||
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
|
||||
_createRefresh(
|
||||
@@ -578,7 +579,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _mobileRightActions(BuildContext context) {
|
||||
List<Widget> _portraitRightActions(BuildContext context) {
|
||||
final model = Provider.of<PeerTabModel>(context);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
|
||||
@@ -699,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
baseOffset: 0,
|
||||
extentOffset: peerSearchTextController.value.text.length);
|
||||
});
|
||||
return Container(
|
||||
width: isMobile ? 120 : 140,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Obx(() => Row(
|
||||
return Obx(() => Container(
|
||||
width: stateGlobal.isPortrait.isTrue ? 120 : 140,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
@@ -766,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
);
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,16 +873,18 @@ class PeerSortDropdown extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
@override
|
||||
void initState() {
|
||||
_PeerSortDropdownState() {
|
||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||
peerSort.value = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: kOptionPeerSorting,
|
||||
v: peerSort.value,
|
||||
);
|
||||
_loadLocalOptions();
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _loadLocalOptions() {
|
||||
peerSort.value = PeerSortType.remoteId;
|
||||
bind.setLocalFlutterOption(
|
||||
k: kOptionPeerSorting,
|
||||
v: peerSort.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
@@ -45,10 +46,14 @@ class LoadEvent {
|
||||
final peerSearchText = "".obs;
|
||||
|
||||
/// for peer sort, global obs value
|
||||
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||
RxString? _peerSort;
|
||||
RxString get peerSort {
|
||||
_peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||
return _peerSort!;
|
||||
}
|
||||
|
||||
// list for listener
|
||||
final obslist = [peerSearchText, peerSort].obs;
|
||||
RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
|
||||
|
||||
final peerSearchTextController =
|
||||
TextEditingController(text: peerSearchText.value);
|
||||
@@ -70,7 +75,8 @@ class _PeersView extends StatefulWidget {
|
||||
}
|
||||
|
||||
/// State for the peer widget.
|
||||
class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
class _PeersViewState extends State<_PeersView>
|
||||
with WindowListener, WidgetsBindingObserver {
|
||||
static const int _maxQueryCount = 3;
|
||||
final HashMap<String, String> _emptyMessages = HashMap.from({
|
||||
LoadEvent.recent: 'empty_recent_tip',
|
||||
@@ -83,8 +89,10 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
var _lastChangeTime = DateTime.now();
|
||||
var _lastQueryPeers = <String>{};
|
||||
var _lastQueryTime = DateTime.now();
|
||||
var _lastWindowRestoreTime = DateTime.now();
|
||||
var _queryCount = 0;
|
||||
var _exit = false;
|
||||
bool _isActive = true;
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@@ -95,12 +103,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
windowManager.removeListener(this);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_exit = true;
|
||||
super.dispose();
|
||||
}
|
||||
@@ -108,15 +118,58 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
_queryCount = 0;
|
||||
_isActive = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
// We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
|
||||
// Maybe it's a bug of the window manager, but the source code seems to be correct.
|
||||
//
|
||||
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
|
||||
// we need the following comparison to ensure that `_isActive` is true in the end.
|
||||
if (isWindows && DateTime.now().difference(_lastWindowRestoreTime) <
|
||||
const Duration(milliseconds: 300)) {
|
||||
return;
|
||||
}
|
||||
_queryCount = _maxQueryCount;
|
||||
_isActive = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowRestore() {
|
||||
// Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
|
||||
// But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
|
||||
if (!isWindows) return;
|
||||
_queryCount = 0;
|
||||
_isActive = true;
|
||||
_lastWindowRestoreTime = DateTime.now();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
_queryCount = _maxQueryCount;
|
||||
// Window minimize also triggers `onWindowBlur()`.
|
||||
}
|
||||
|
||||
// This function is required for mobile.
|
||||
// `onWindowFocus` works fine for desktop.
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (isDesktop || isWebDesktop) return;
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_isActive = true;
|
||||
_queryCount = 0;
|
||||
} else if (state == AppLifecycleState.inactive) {
|
||||
_isActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
|
||||
// Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
|
||||
// Simple demo can reproduce this issue.
|
||||
return ChangeNotifierProvider<Peers>(
|
||||
create: (context) => widget.peers,
|
||||
child: Consumer<Peers>(builder: (context, peers, child) {
|
||||
@@ -172,7 +225,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
var peers = snapshot.data!;
|
||||
if (peers.length > 1000) peers = peers.sublist(0, 1000);
|
||||
gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
|
||||
buildOnePeer(Peer peer) {
|
||||
buildOnePeer(Peer peer, bool isPortrait) {
|
||||
final visibilityChild = VisibilityDetector(
|
||||
key: ValueKey(_cardId(peer.id)),
|
||||
onVisibilityChanged: onVisibilityChanged,
|
||||
@@ -184,7 +237,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
// No need to listen the currentTab change event.
|
||||
// Because the currentTab change event will trigger the peers change event,
|
||||
// and the peers change event will trigger _buildPeersView().
|
||||
return (isDesktop || isWebDesktop)
|
||||
return !isPortrait
|
||||
? Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? Container(height: 45, child: visibilityChild)
|
||||
: peerCardUiType.value == PeerUiType.grid
|
||||
@@ -195,44 +248,45 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
: Container(child: visibilityChild);
|
||||
}
|
||||
|
||||
final Widget child;
|
||||
if (isMobile) {
|
||||
child = ListView.builder(
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]).marginOnly(
|
||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child = Obx(() => peerCardUiType.value == PeerUiType.list
|
||||
? DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]).marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
}),
|
||||
)
|
||||
: DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index]);
|
||||
}),
|
||||
));
|
||||
}
|
||||
// We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
|
||||
// Continious rebuilds of `ListView.builder` will cause memory leak.
|
||||
// Simple demo can reproduce this issue.
|
||||
final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
|
||||
? ListView.builder(
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], true).marginOnly(
|
||||
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||
},
|
||||
)
|
||||
: peerCardUiType.value == PeerUiType.list
|
||||
? DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false)
|
||||
.marginOnly(
|
||||
right: space,
|
||||
top: index == 0 ? 0 : space / 2,
|
||||
bottom: space / 2);
|
||||
}),
|
||||
)
|
||||
: DesktopScrollWrapper(
|
||||
scrollController: _scrollController,
|
||||
child: DynamicGridView.builder(
|
||||
controller: _scrollController,
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithWrapping(
|
||||
mainAxisSpacing: space / 2,
|
||||
crossAxisSpacing: space),
|
||||
itemCount: peers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildOnePeer(peers[index], false);
|
||||
}),
|
||||
));
|
||||
|
||||
if (updateEvent == UpdateEvent.load) {
|
||||
_curPeers.clear();
|
||||
@@ -268,7 +322,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
_queryOnlines(false);
|
||||
}
|
||||
} else {
|
||||
if (_queryCount < _maxQueryCount || !p) {
|
||||
if (_isActive && (_queryCount < _maxQueryCount || !p)) {
|
||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
@@ -286,14 +340,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
||||
_queryOnlines(bool isLoadEvent) {
|
||||
if (_curPeers.isNotEmpty) {
|
||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||
_lastQueryPeers = {..._curPeers};
|
||||
if (isLoadEvent) {
|
||||
_lastChangeTime = DateTime.now();
|
||||
} else {
|
||||
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
||||
}
|
||||
_queryCount = 0;
|
||||
}
|
||||
_lastQueryPeers = {..._curPeers};
|
||||
if (isLoadEvent) {
|
||||
_lastChangeTime = DateTime.now();
|
||||
} else {
|
||||
_lastQueryTime = DateTime.now().subtract(_queryInterval);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Peer>>? matchPeers(
|
||||
|
||||
@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// https://github.com/flutter/flutter/issues/154053
|
||||
final useRawKeyEvents = isLinux && !isWeb;
|
||||
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
|
||||
// while `Alt` and `Control` are seperated key events for en-US input method.
|
||||
return FocusScope(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
canRequestFocus: true,
|
||||
focusNode: focusNode,
|
||||
onFocusChange: onFocusChange,
|
||||
onKey: (FocusNode data, RawKeyEvent e) =>
|
||||
inputModel.handleRawKeyEvent(e),
|
||||
onKey: useRawKeyEvents
|
||||
? (FocusNode data, RawKeyEvent event) =>
|
||||
inputModel.handleRawKeyEvent(event)
|
||||
: null,
|
||||
onKeyEvent: useRawKeyEvents
|
||||
? null
|
||||
: (FocusNode node, KeyEvent event) =>
|
||||
inputModel.handleKeyEvent(event),
|
||||
child: child));
|
||||
}
|
||||
}
|
||||
@@ -233,7 +243,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||
}
|
||||
// Workaround for the issue that the first pan event is sent a long time after the start event.
|
||||
@@ -275,7 +285,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
if (isDesktop || isWebDesktop) {
|
||||
ffi.cursorModel.clearRemoteWindowCoords();
|
||||
}
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
|
||||
@@ -130,12 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// paste
|
||||
if (isMobile &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
perms['keyboard'] != false &&
|
||||
perms['clipboard'] != false) {
|
||||
if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Paste')),
|
||||
child: Text(translate('Send clipboard keystrokes')),
|
||||
onPressed: () async {
|
||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
|
||||
@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
const String kPeerPlatformMacOS = "Mac OS";
|
||||
const String kPeerPlatformAndroid = "Android";
|
||||
const String kPeerPlatformWebDesktop = "WebDesktop";
|
||||
|
||||
const double kScrollbarThickness = 12.0;
|
||||
|
||||
@@ -136,6 +137,7 @@ const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||
const String kOptionStopService = "stop-service";
|
||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
|
||||
|
||||
// buildin opitons
|
||||
const String kOptionHideServerSetting = "hide-server-settings";
|
||||
@@ -240,9 +242,9 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
|
||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||
const kFullScreenEdgeSize = 0.0;
|
||||
const kMaximizeEdgeSize = 0.0;
|
||||
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
||||
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||
final kWindowBorderWidth = 1.0;
|
||||
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
|
||||
const kWindowResizeEdgeSize = 5.0;
|
||||
const kWindowBorderWidth = 1.0;
|
||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||
const kFrameBorderRadius = 12.0;
|
||||
const kFrameClipRRectBorderRadius = 12.0;
|
||||
|
||||
@@ -169,16 +169,12 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
|
||||
final status =
|
||||
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
|
||||
final statusNum = status['status_num'] as int;
|
||||
final preStatus = stateGlobal.svcStatus.value;
|
||||
if (statusNum == 0) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.connecting;
|
||||
} else if (statusNum == -1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
} else if (statusNum == 1) {
|
||||
stateGlobal.svcStatus.value = SvcStatus.ready;
|
||||
if (preStatus != SvcStatus.ready) {
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
}
|
||||
} else {
|
||||
stateGlobal.svcStatus.value = SvcStatus.notReady;
|
||||
}
|
||||
@@ -212,14 +208,14 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
if (lastRemoteId != _idController.id) {
|
||||
setState(() {
|
||||
_idController.id = lastRemoteId;
|
||||
});
|
||||
}
|
||||
}();
|
||||
});
|
||||
}
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
windowManager.addListener(this);
|
||||
@@ -261,8 +257,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
@override
|
||||
void onWindowLeaveFullScreen() {
|
||||
// Restore edge border to default edge size.
|
||||
stateGlobal.resizeEdgeSize.value =
|
||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
|
||||
stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
|
||||
? kMaximizeEdgeSize
|
||||
: windowResizeEdgeSize;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -857,6 +857,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
// SpecialCharacterValidationRule(),
|
||||
MinCharactersValidationRule(8),
|
||||
];
|
||||
final maxLength = bind.mainMaxEncryptLen();
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
@@ -915,6 +916,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
errMsg0 = '';
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -941,6 +943,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||
errMsg1 = '';
|
||||
});
|
||||
},
|
||||
maxLength: maxLength,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -61,7 +61,8 @@ class DesktopSettingPage extends StatefulWidget {
|
||||
final SettingsTabKey initialTabkey;
|
||||
static final List<SettingsTabKey> tabKeys = [
|
||||
SettingsTabKey.general,
|
||||
if (!bind.isOutgoingOnly() &&
|
||||
if (!isWeb &&
|
||||
!bind.isOutgoingOnly() &&
|
||||
!bind.isDisableSettings() &&
|
||||
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
||||
SettingsTabKey.safety,
|
||||
@@ -78,7 +79,8 @@ class DesktopSettingPage extends StatefulWidget {
|
||||
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
|
||||
State<DesktopSettingPage> createState() =>
|
||||
_DesktopSettingPageState(initialTabkey);
|
||||
|
||||
static void switch2page(SettingsTabKey page) {
|
||||
try {
|
||||
@@ -111,10 +113,8 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
|
||||
_DesktopSettingPageState(SettingsTabKey initialTabkey) {
|
||||
var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
|
||||
if (initialIndex == -1) {
|
||||
initialIndex = 0;
|
||||
}
|
||||
@@ -217,7 +217,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
width: _kTabWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
_header(),
|
||||
_header(context),
|
||||
Flexible(child: _listView(tabs: _settingTabs())),
|
||||
],
|
||||
),
|
||||
@@ -240,21 +240,40 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _header() {
|
||||
Widget _header(BuildContext context) {
|
||||
final settingsText = Text(
|
||||
translate('Settings'),
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: _accentColor,
|
||||
fontSize: _kTitleFontSize,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 62,
|
||||
child: Text(
|
||||
translate('Settings'),
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: _accentColor,
|
||||
fontSize: _kTitleFontSize,
|
||||
fontWeight: FontWeight.w400,
|
||||
if (isWeb)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.arrow_back),
|
||||
).marginOnly(left: 5),
|
||||
if (isWeb)
|
||||
SizedBox(
|
||||
height: 62,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: settingsText,
|
||||
),
|
||||
),
|
||||
).marginOnly(left: 20, top: 10),
|
||||
).marginOnly(left: 20),
|
||||
if (!isWeb)
|
||||
SizedBox(
|
||||
height: 62,
|
||||
child: settingsText,
|
||||
).marginOnly(left: 20, top: 10),
|
||||
const Spacer(),
|
||||
],
|
||||
);
|
||||
@@ -323,7 +342,8 @@ class _General extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GeneralState extends State<_General> {
|
||||
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service');
|
||||
final RxBool serviceStop =
|
||||
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
|
||||
RxBool serviceBtnEnabled = true.obs;
|
||||
|
||||
@override
|
||||
@@ -335,13 +355,13 @@ class _GeneralState extends State<_General> {
|
||||
physics: DraggableNeverScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
children: [
|
||||
service(),
|
||||
if (!isWeb) service(),
|
||||
theme(),
|
||||
_Card(title: 'Language', children: [language()]),
|
||||
hwcodec(),
|
||||
audio(context),
|
||||
record(context),
|
||||
WaylandCard(),
|
||||
if (!isWeb) hwcodec(),
|
||||
if (!isWeb) audio(context),
|
||||
if (!isWeb) record(context),
|
||||
if (!isWeb) WaylandCard(),
|
||||
other()
|
||||
],
|
||||
).marginOnly(bottom: _kListViewBottomMargin));
|
||||
@@ -395,13 +415,13 @@ class _GeneralState extends State<_General> {
|
||||
|
||||
Widget other() {
|
||||
final children = <Widget>[
|
||||
if (!bind.isIncomingOnly())
|
||||
if (!isWeb && !bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
kOptionEnableConfirmClosingTabs,
|
||||
isServer: false),
|
||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||
wallpaper(),
|
||||
if (!bind.isIncomingOnly()) ...[
|
||||
if (!isWeb) wallpaper(),
|
||||
if (!isWeb && !bind.isIncomingOnly()) ...[
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Open connection in new tab',
|
||||
@@ -418,18 +438,19 @@ class _GeneralState extends State<_General> {
|
||||
kOptionAllowAlwaysSoftwareRender,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: translate('texture_render_tip'),
|
||||
child: _OptionCheckBox(
|
||||
context,
|
||||
"Use texture rendering",
|
||||
kOptionTextureRender,
|
||||
optGetter: bind.mainGetUseTextureRender,
|
||||
optSetter: (k, v) async =>
|
||||
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
|
||||
if (!isWeb)
|
||||
Tooltip(
|
||||
message: translate('texture_render_tip'),
|
||||
child: _OptionCheckBox(
|
||||
context,
|
||||
"Use texture rendering",
|
||||
kOptionTextureRender,
|
||||
optGetter: bind.mainGetUseTextureRender,
|
||||
optSetter: (k, v) async =>
|
||||
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!bind.isCustomClient())
|
||||
if (!isWeb && !bind.isCustomClient())
|
||||
_OptionCheckBox(
|
||||
context,
|
||||
'Check for software update on startup',
|
||||
@@ -444,7 +465,7 @@ class _GeneralState extends State<_General> {
|
||||
)
|
||||
],
|
||||
];
|
||||
if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||
children.add(_OptionCheckBox(
|
||||
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||
}
|
||||
@@ -516,16 +537,16 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
builder(devices, currentDevice, setDevice) {
|
||||
return _Card(title: 'Audio Input Device', children: [
|
||||
...devices.map((device) => _Radio<String>(context,
|
||||
value: device,
|
||||
groupValue: currentDevice,
|
||||
autoNewLine: false,
|
||||
label: device, onChanged: (value) {
|
||||
setDevice(value);
|
||||
setState(() {});
|
||||
}))
|
||||
]);
|
||||
final child = ComboBox(
|
||||
keys: devices,
|
||||
values: devices,
|
||||
initialKey: currentDevice,
|
||||
onChanged: (key) async {
|
||||
setDevice(key);
|
||||
setState(() {});
|
||||
},
|
||||
).marginOnly(left: _kContentHMargin);
|
||||
return _Card(title: 'Audio Input Device', children: [child]);
|
||||
}
|
||||
|
||||
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
|
||||
@@ -642,8 +663,9 @@ class _GeneralState extends State<_General> {
|
||||
initialKey: currentKey,
|
||||
onChanged: (key) async {
|
||||
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
|
||||
reloadAllWindows();
|
||||
bind.mainChangeLanguage(lang: key);
|
||||
if (isWeb) reloadCurrentWindow();
|
||||
if (!isWeb) reloadAllWindows();
|
||||
if (!isWeb) bind.mainChangeLanguage(lang: key);
|
||||
},
|
||||
enabled: !isOptFixed,
|
||||
).marginOnly(left: _kContentHMargin);
|
||||
@@ -784,8 +806,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
onChangedBot(!hasBot.value);
|
||||
},
|
||||
).marginOnly(left: _kCheckBoxLeftMargin + 30);
|
||||
|
||||
final trust = Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: translate("enable-trusted-devices-tip"),
|
||||
child: _OptionCheckBox(context, "Enable trusted devices",
|
||||
kOptionEnableTrustedDevices,
|
||||
enabled: !locked, update: (v) {
|
||||
setState(() {});
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (mainGetBoolOptionSync(kOptionEnableTrustedDevices))
|
||||
ElevatedButton(
|
||||
onPressed: locked
|
||||
? null
|
||||
: () {
|
||||
manageTrustedDeviceDialog();
|
||||
},
|
||||
child: Text(translate('Manage trusted devices')))
|
||||
],
|
||||
).marginOnly(left: 30);
|
||||
|
||||
return Column(
|
||||
children: [tfa, bot],
|
||||
children: [tfa, bot, trust],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1019,6 +1066,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
|
||||
'allow-only-conn-window-open',
|
||||
reverse: false, enabled: enabled),
|
||||
if (bind.mainIsInstalled()) unlockPin()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1266,6 +1314,40 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
}(),
|
||||
];
|
||||
}
|
||||
|
||||
Widget unlockPin() {
|
||||
bool enabled = !locked;
|
||||
RxString unlockPin = bind.mainGetUnlockPin().obs;
|
||||
update() async {
|
||||
unlockPin.value = bind.mainGetUnlockPin();
|
||||
}
|
||||
|
||||
onChanged(bool? checked) async {
|
||||
changeUnlockPinDialog(unlockPin.value, update);
|
||||
}
|
||||
|
||||
final isOptFixed = isOptionFixed(kOptionWhitelist);
|
||||
return GestureDetector(
|
||||
child: Obx(() => Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: unlockPin.isNotEmpty,
|
||||
onChanged: enabled && !isOptFixed ? onChanged : null)
|
||||
.marginOnly(right: 5),
|
||||
Expanded(
|
||||
child: Text(
|
||||
translate('Unlock with PIN'),
|
||||
style: TextStyle(color: disabledTextColor(context, enabled)),
|
||||
))
|
||||
],
|
||||
)),
|
||||
onTap: enabled
|
||||
? () {
|
||||
onChanged(!unlockPin.isNotEmpty);
|
||||
}
|
||||
: null,
|
||||
).marginOnly(left: _kCheckBoxLeftMargin);
|
||||
}
|
||||
}
|
||||
|
||||
class _Network extends StatefulWidget {
|
||||
@@ -1278,7 +1360,7 @@ class _Network extends StatefulWidget {
|
||||
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
bool locked = bind.mainIsInstalled();
|
||||
bool locked = !isWeb && bind.mainIsInstalled();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1287,8 +1369,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
final scrollController = ScrollController();
|
||||
final hideServer =
|
||||
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||
// TODO: support web proxy
|
||||
final hideProxy =
|
||||
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
return DesktopScrollWrapper(
|
||||
scrollController: scrollController,
|
||||
child: ListView(
|
||||
@@ -1408,7 +1491,7 @@ class _DisplayState extends State<_Display> {
|
||||
scrollStyle(context),
|
||||
imageQuality(context),
|
||||
codec(context),
|
||||
privacyModeImpl(context),
|
||||
if (!isWeb) privacyModeImpl(context),
|
||||
other(context),
|
||||
]).marginOnly(bottom: _kListViewBottomMargin));
|
||||
}
|
||||
@@ -1819,9 +1902,10 @@ class _AboutState extends State<_About> {
|
||||
SelectionArea(
|
||||
child: Text('${translate('Build Date')}: $buildDate')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
SelectionArea(
|
||||
child: Text('${translate('Fingerprint')}: $fingerprint')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
if (!isWeb)
|
||||
SelectionArea(
|
||||
child: Text('${translate('Fingerprint')}: $fingerprint')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com/privacy.html');
|
||||
@@ -2161,9 +2245,14 @@ Widget _lock(
|
||||
Text(translate(label)).marginOnly(left: 5),
|
||||
]).marginSymmetric(vertical: 2)),
|
||||
onPressed: () async {
|
||||
bool checked = await callMainCheckSuperUserPermission();
|
||||
if (checked) {
|
||||
onUnlock();
|
||||
final unlockPin = bind.mainGetUnlockPin();
|
||||
if (unlockPin.isEmpty) {
|
||||
bool checked = await callMainCheckSuperUserPermission();
|
||||
if (checked) {
|
||||
onUnlock();
|
||||
}
|
||||
} else {
|
||||
checkUnlockPinDialog(unlockPin, onUnlock);
|
||||
}
|
||||
},
|
||||
).marginSymmetric(horizontal: 2, vertical: 4),
|
||||
@@ -2423,6 +2512,7 @@ void changeSocks5Proxy() async {
|
||||
: Icons.visibility))),
|
||||
controller: pwdController,
|
||||
enabled: !isOptFixed,
|
||||
maxLength: bind.mainMaxEncryptLen(),
|
||||
)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -44,21 +44,9 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
||||
final RxBool _block = false.obs;
|
||||
// bool mouseIn = false;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
shouldBeBlocked(_block, canBeBlocked);
|
||||
} else if (state == AppLifecycleState.inactive) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
_DesktopTabPageState() {
|
||||
RemoteCountState.init();
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelHomePage,
|
||||
label: kTabLabelHomePage,
|
||||
@@ -81,6 +69,21 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
shouldBeBlocked(_block, canBeBlocked);
|
||||
} else if (state == AppLifecycleState.inactive) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
/*
|
||||
bool _handleKeyEvent(KeyEvent event) {
|
||||
if (!mouseIn && event is KeyDownEvent) {
|
||||
@@ -123,6 +126,7 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
||||
: Obx(
|
||||
() => DragToResizeArea(
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||
child: tabWidget,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:extended_text/extended_text.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
@@ -68,7 +69,7 @@ class FileManagerPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FileManagerPageState extends State<FileManagerPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
||||
|
||||
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
||||
@@ -98,7 +99,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
debugPrint("File manager page init success with id ${widget.id}");
|
||||
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -111,12 +116,21 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
}
|
||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||
});
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
jobController.jobTable.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@@ -170,10 +184,25 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
/// transfer status list
|
||||
/// watch transfer status
|
||||
Widget statusList() {
|
||||
Widget getIcon(JobProgress job) {
|
||||
final color = Theme.of(context).tabBarTheme.labelColor;
|
||||
switch (job.type) {
|
||||
case JobType.deleteDir:
|
||||
case JobType.deleteFile:
|
||||
return Icon(Icons.delete_outline, color: color);
|
||||
default:
|
||||
return Transform.rotate(
|
||||
angle: job.isRemoteToLocal ? pi : 0,
|
||||
child: Icon(Icons.arrow_forward_ios, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
statusListView(List<JobProgress> jobs) => ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = jobs[index];
|
||||
final status = item.getStatus();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
child: generateCard(
|
||||
@@ -183,15 +212,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemoteToLocal ? pi : 0,
|
||||
child: SvgPicture.asset("assets/arrow.svg",
|
||||
colorFilter: svgColor(
|
||||
Theme.of(context).tabBarTheme.labelColor)),
|
||||
).paddingOnly(left: 15),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
getIcon(item)
|
||||
.marginSymmetric(horizontal: 10, vertical: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -200,45 +222,28 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: item.jobName,
|
||||
child: Text(
|
||||
item.fileName,
|
||||
child: ExtendedText(
|
||||
item.jobName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
Text(
|
||||
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
overflowWidget: TextOverflowWidget(
|
||||
child: Text("..."),
|
||||
position: TextOverflowPosition.start),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state != JobState.inProgress,
|
||||
child: Text(
|
||||
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
waitDuration: Duration(milliseconds: 500),
|
||||
message: status,
|
||||
child: Text(status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
)).marginOnly(top: 6),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state == JobState.inProgress,
|
||||
child: Text(
|
||||
translate(
|
||||
item.display(),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: item.state != JobState.inProgress,
|
||||
offstage: item.type != JobType.transfer ||
|
||||
item.state != JobState.inProgress,
|
||||
child: LinearPercentIndicator(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
animateFromLastPercent: true,
|
||||
center: Text(
|
||||
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
||||
@@ -248,7 +253,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
progressColor: MyTheme.accent,
|
||||
backgroundColor: Theme.of(context).hoverColor,
|
||||
lineHeight: kDesktopFileTransferRowHeight,
|
||||
).paddingSymmetric(vertical: 15),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -259,6 +264,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Offstage(
|
||||
offstage: item.state != JobState.paused,
|
||||
child: MenuButton(
|
||||
tooltip: translate("Resume"),
|
||||
onPressed: () {
|
||||
jobController.resumeJob(item.id);
|
||||
},
|
||||
@@ -271,7 +277,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
),
|
||||
),
|
||||
MenuButton(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
tooltip: translate("Delete"),
|
||||
child: SvgPicture.asset(
|
||||
"assets/close.svg",
|
||||
colorFilter: svgColor(Colors.white),
|
||||
@@ -284,11 +290,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
hoverColor: MyTheme.accent80,
|
||||
),
|
||||
],
|
||||
),
|
||||
).marginAll(12),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -518,6 +524,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
Row(
|
||||
children: [
|
||||
MenuButton(
|
||||
tooltip: translate('Back'),
|
||||
padding: EdgeInsets.only(
|
||||
right: 3,
|
||||
),
|
||||
@@ -537,6 +544,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate('Parent directory'),
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: SvgPicture.asset(
|
||||
@@ -601,6 +609,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
switch (_locationStatus.value) {
|
||||
case LocationStatus.bread:
|
||||
return MenuButton(
|
||||
tooltip: translate('Search'),
|
||||
onPressed: () {
|
||||
_locationStatus.value = LocationStatus.fileSearchBar;
|
||||
Future.delayed(
|
||||
@@ -627,6 +636,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
);
|
||||
case LocationStatus.fileSearchBar:
|
||||
return MenuButton(
|
||||
tooltip: translate('Clear'),
|
||||
onPressed: () {
|
||||
onSearchText("", isLocal);
|
||||
_locationStatus.value = LocationStatus.bread;
|
||||
@@ -642,6 +652,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
}
|
||||
}),
|
||||
MenuButton(
|
||||
tooltip: translate('Refresh File'),
|
||||
padding: EdgeInsets.only(
|
||||
left: 3,
|
||||
),
|
||||
@@ -667,6 +678,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||
children: [
|
||||
MenuButton(
|
||||
tooltip: translate('Home'),
|
||||
padding: EdgeInsets.only(
|
||||
right: 3,
|
||||
),
|
||||
@@ -682,11 +694,27 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate('Create Folder'),
|
||||
onPressed: () {
|
||||
final name = TextEditingController();
|
||||
String? errorText;
|
||||
_ffi.dialogManager.show((setState, close, context) {
|
||||
name.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
submit() {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
if (!PathUtil.validName(name.value.text,
|
||||
controller.options.value.isWindows)) {
|
||||
setState(() {
|
||||
errorText = translate("Invalid folder name");
|
||||
});
|
||||
return;
|
||||
}
|
||||
controller.createDir(PathUtil.join(
|
||||
controller.directory.value.path,
|
||||
name.value.text,
|
||||
@@ -718,6 +746,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
labelText: translate(
|
||||
"Please enter the folder name",
|
||||
),
|
||||
errorText: errorText,
|
||||
),
|
||||
controller: name,
|
||||
autofocus: true,
|
||||
@@ -751,6 +780,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
),
|
||||
Obx(() => MenuButton(
|
||||
tooltip: translate('Delete'),
|
||||
onPressed: SelectedItems.valid(selectedItems.items)
|
||||
? () async {
|
||||
await (controller
|
||||
@@ -882,6 +912,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
child: MenuButton(
|
||||
tooltip: translate('More'),
|
||||
onPressed: () => mod_menu.showMenu(
|
||||
context: context,
|
||||
position: menuPos,
|
||||
@@ -913,6 +944,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
BuildContext context, ScrollController scrollController) {
|
||||
final fd = controller.directory.value;
|
||||
final entries = fd.entries;
|
||||
Rx<Entry?> rightClickEntry = Rx(null);
|
||||
|
||||
return ListSearchActionListener(
|
||||
node: _keyboardNode,
|
||||
@@ -971,16 +1003,70 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
final lastModifiedStr = entry.isDrive
|
||||
? " "
|
||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
onTap() {
|
||||
final items = selectedItems;
|
||||
// handle double click
|
||||
if (_checkDoubleClick(entry)) {
|
||||
controller.openDirectory(entry.path);
|
||||
items.clear();
|
||||
return;
|
||||
}
|
||||
_onSelectedChanged(items, filteredEntries, entry, isLocal);
|
||||
}
|
||||
|
||||
onSecondaryTap() {
|
||||
final items = [
|
||||
if (!entry.isDrive &&
|
||||
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
|
||||
mod_menu.PopupMenuItem(
|
||||
child: Text("Rename"),
|
||||
height: CustomPopupMenuTheme.height,
|
||||
onTap: () {
|
||||
controller.renameAction(entry, isLocal);
|
||||
},
|
||||
)
|
||||
];
|
||||
if (items.isNotEmpty) {
|
||||
rightClickEntry.value = entry;
|
||||
final future = mod_menu.showMenu(
|
||||
context: context,
|
||||
position: secondaryPosition,
|
||||
items: items,
|
||||
);
|
||||
future.then((value) {
|
||||
rightClickEntry.value = null;
|
||||
});
|
||||
future.onError((error, stackTrace) {
|
||||
rightClickEntry.value = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSecondaryTapDown(details) {
|
||||
secondaryPosition = RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 1),
|
||||
child: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: selectedItems.items.contains(entry)
|
||||
? Theme.of(context).hoverColor
|
||||
? MyTheme.button
|
||||
: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5.0),
|
||||
),
|
||||
border: rightClickEntry.value == entry
|
||||
? Border.all(
|
||||
color: MyTheme.button,
|
||||
width: 1.0,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
key: ValueKey(entry.name),
|
||||
height: kDesktopFileTransferRowHeight,
|
||||
@@ -1019,22 +1105,19 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
),
|
||||
Expanded(
|
||||
child: Text(entry.name.nonBreaking,
|
||||
style: TextStyle(
|
||||
color: selectedItems.items
|
||||
.contains(entry)
|
||||
? Colors.white
|
||||
: null),
|
||||
overflow:
|
||||
TextOverflow.ellipsis))
|
||||
]),
|
||||
)),
|
||||
),
|
||||
onTap: () {
|
||||
final items = selectedItems;
|
||||
// handle double click
|
||||
if (_checkDoubleClick(entry)) {
|
||||
controller.openDirectory(entry.path);
|
||||
items.clear();
|
||||
return;
|
||||
}
|
||||
_onSelectedChanged(
|
||||
items, filteredEntries, entry, isLocal);
|
||||
},
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2.0,
|
||||
@@ -1051,11 +1134,17 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
color: selectedItems.items
|
||||
.contains(entry)
|
||||
? Colors.white70
|
||||
: MyTheme.darkGray,
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
// Divider from header.
|
||||
SizedBox(
|
||||
@@ -1071,9 +1160,16 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
sizeStr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10, color: MyTheme.darkGray),
|
||||
fontSize: 10,
|
||||
color:
|
||||
selectedItems.items.contains(entry)
|
||||
? Colors.white70
|
||||
: MyTheme.darkGray),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -34,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@@ -54,8 +55,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
|
||||
@@ -97,6 +96,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
));
|
||||
final tabWidget = isLinux
|
||||
@@ -111,6 +111,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
|
||||
class _InstallPageState extends State<InstallPage> {
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_InstallPageState() {
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
const label = "install";
|
||||
tabController.add(TabInfo(
|
||||
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return DragToResizeArea(
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: windowManagerEnableResizeEdges,
|
||||
child: Container(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
||||
);
|
||||
|
||||
_InstallPageBodyState() {
|
||||
controller = TextEditingController(text: bind.installInstallPath());
|
||||
final installOptions = jsonDecode(bind.installInstallOptions());
|
||||
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
|
||||
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
controller = TextEditingController(text: bind.installInstallPath());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
if (desktopicon.value) args += ' desktopicon';
|
||||
bind.installInstallMe(options: args, path: controller.text);
|
||||
}
|
||||
|
||||
do_install();
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,10 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
isRdp: widget.isRDP);
|
||||
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||
debugPrint("Port forward page init success with id ${widget.id}");
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -34,6 +34,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@@ -54,8 +55,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
debugPrint(
|
||||
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
@@ -106,6 +105,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
return true;
|
||||
},
|
||||
tail: AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
),
|
||||
);
|
||||
@@ -127,6 +127,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
() => SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -45,7 +45,9 @@ class RemotePage extends StatefulWidget {
|
||||
this.switchUuid,
|
||||
this.forceRelay,
|
||||
this.isSharedPassword,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key) {
|
||||
initSharedStates(id);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final SessionID? sessionId;
|
||||
@@ -64,7 +66,7 @@ class RemotePage extends StatefulWidget {
|
||||
|
||||
@override
|
||||
State<RemotePage> createState() {
|
||||
final state = _RemotePageState();
|
||||
final state = _RemotePageState(id);
|
||||
_lastState.value = state;
|
||||
return state;
|
||||
}
|
||||
@@ -94,8 +96,11 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
SessionID get sessionId => _ffi.sessionId;
|
||||
|
||||
_RemotePageState(String id) {
|
||||
_initStates(id);
|
||||
}
|
||||
|
||||
void _initStates(String id) {
|
||||
initSharedStates(id);
|
||||
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
@@ -105,7 +110,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initStates(widget.id);
|
||||
_ffi = FFI(widget.sessionId);
|
||||
Get.put<FFI>(_ffi, tag: widget.id);
|
||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||
@@ -135,11 +139,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
||||
// Session option should be set after models.dart/FFI.start
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Session option should be set after models.dart/FFI.start
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||
});
|
||||
DesktopMultiWindow.addListener(this);
|
||||
// if (!_isCustomCursorInited) {
|
||||
// customCursorController.registerNeedUpdateCursorCallback(
|
||||
@@ -154,7 +160,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
// }
|
||||
|
||||
_blockableOverlayState.applyFfi(_ffi);
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -236,8 +245,10 @@ class _RemotePageState extends State<RemotePage>
|
||||
super.dispose();
|
||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||
// ensure we leave this session, this is a double check
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
if (closeSession) {
|
||||
// ensure we leave this session, this is a double check
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
}
|
||||
DesktopMultiWindow.removeListener(this);
|
||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||
_ffi.imageModel.disposeImage();
|
||||
@@ -565,11 +576,6 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
|
||||
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final m = Provider.of<ImageModel>(context);
|
||||
|
||||
@@ -71,7 +71,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final ffi = remotePage.ffi;
|
||||
bind.setCurSessionId(sessionId: ffi.sessionId);
|
||||
}
|
||||
WindowController.fromWindowId(windowId())
|
||||
WindowController.fromWindowId(params['windowId'])
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
UnreadChatCountState.find(id).value = 0;
|
||||
};
|
||||
@@ -98,15 +98,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
));
|
||||
_update_remote_count();
|
||||
}
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
|
||||
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||
if (!_isScreenRectSet) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(
|
||||
@@ -121,11 +120,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = Scaffold(
|
||||
@@ -134,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||
@@ -233,6 +228,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
// Specially configured for a better resize area and remote control.
|
||||
childPadding: kDragToResizeAreaPadding,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
));
|
||||
}
|
||||
@@ -413,12 +409,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final display = args['display'];
|
||||
final displays = args['displays'];
|
||||
final screenRect = parseParamScreenRect(args);
|
||||
final prePeerCount = tabController.length;
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (stateGlobal.fullscreen.isTrue) {
|
||||
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||
stateGlobal.setFullscreen(false, procWnd: false);
|
||||
}
|
||||
await setNewConnectWindowFrame(windowId(), id!, display, screenRect);
|
||||
await setNewConnectWindowFrame(
|
||||
windowId(), id!, prePeerCount, display, screenRect);
|
||||
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||
await windowOnTop(windowId());
|
||||
});
|
||||
|
||||
@@ -32,14 +32,18 @@ class DesktopServerPage extends StatefulWidget {
|
||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
final tabController = gFFI.serverModel.tabController;
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
_DesktopServerPageState() {
|
||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
||||
windowManager.addListener(this);
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
tabController.onRemoved = (_, id) {
|
||||
onRemoveId(id);
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -79,7 +83,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, serverModel, child) {
|
||||
final body = Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: ConnectionManager(),
|
||||
);
|
||||
return isLinux
|
||||
@@ -108,6 +112,28 @@ class ConnectionManagerState extends State<ConnectionManager>
|
||||
with WidgetsBindingObserver {
|
||||
final RxBool _block = false.obs;
|
||||
|
||||
ConnectionManagerState() {
|
||||
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
||||
final client_id = int.tryParse(client_id_str);
|
||||
if (client_id != null) {
|
||||
final client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == client_id);
|
||||
if (client != null) {
|
||||
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
||||
if (client.unreadChatMessageCount.value > 0) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
client.unreadChatMessageCount.value = 0;
|
||||
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
||||
});
|
||||
}
|
||||
windowManager.setTitle(getWindowNameWithId(client.peerId));
|
||||
gFFI.cmFileModel.updateCurrentClientId(client.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
gFFI.chatModel.isConnManager = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
@@ -121,25 +147,6 @@ class ConnectionManagerState extends State<ConnectionManager>
|
||||
@override
|
||||
void initState() {
|
||||
gFFI.serverModel.updateClientState();
|
||||
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
||||
final client_id = int.tryParse(client_id_str);
|
||||
if (client_id != null) {
|
||||
final client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == client_id);
|
||||
if (client != null) {
|
||||
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
||||
if (client.unreadChatMessageCount.value > 0) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
client.unreadChatMessageCount.value = 0;
|
||||
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
||||
});
|
||||
}
|
||||
windowManager.setTitle(getWindowNameWithId(client.peerId));
|
||||
gFFI.cmFileModel.updateCurrentClientId(client.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
gFFI.chatModel.isConnManager = true;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
super.initState();
|
||||
}
|
||||
@@ -186,8 +193,6 @@ class ConnectionManagerState extends State<ConnectionManager>
|
||||
maxLabelWidth: 100,
|
||||
tail: null, //buildScrollJumper(),
|
||||
blockTab: allowRemoteCMModification() ? null : _block,
|
||||
selectedTabBackgroundColor:
|
||||
Theme.of(context).hintColor.withOpacity(0),
|
||||
tabBuilder: (key, icon, label, themeConf) {
|
||||
final client = serverModel.clients
|
||||
.firstWhereOrNull((client) => client.id.toString() == key);
|
||||
@@ -222,7 +227,7 @@ class ConnectionManagerState extends State<ConnectionManager>
|
||||
borderWidth;
|
||||
final realChatPageWidth =
|
||||
constrains.maxWidth - realClosedWidth;
|
||||
return Row(children: [
|
||||
final row = Row(children: [
|
||||
if (constrains.maxWidth >
|
||||
kConnectionManagerWindowSizeClosedChat.width)
|
||||
Consumer<ChatModel>(
|
||||
@@ -240,6 +245,10 @@ class ConnectionManagerState extends State<ConnectionManager>
|
||||
child:
|
||||
SizedBox(width: realClosedWidth, child: pageView)),
|
||||
]);
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: row,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -399,7 +408,10 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
_time.value = _time.value + 1;
|
||||
}
|
||||
});
|
||||
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
||||
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -732,7 +744,8 @@ class _CmControlPanel extends StatelessWidget {
|
||||
child: buildButton(context,
|
||||
color: MyTheme.accent,
|
||||
onClick: null, onTapDown: (details) async {
|
||||
final devicesInfo = await AudioInput.getDevicesInfo(true, true);
|
||||
final devicesInfo =
|
||||
await AudioInput.getDevicesInfo(true, true);
|
||||
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||
if (devices.isEmpty) {
|
||||
msgBox(
|
||||
@@ -764,7 +777,8 @@ class _CmControlPanel extends StatelessWidget {
|
||||
value: d,
|
||||
groupValue: currentDevice,
|
||||
onChanged: (v) {
|
||||
if (v != null) AudioInput.setDevice(v, true, true);
|
||||
if (v != null)
|
||||
AudioInput.setDevice(v, true, true);
|
||||
},
|
||||
child: Container(
|
||||
child: Text(
|
||||
@@ -1143,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
Text(translate('Create Folder'))
|
||||
],
|
||||
);
|
||||
case CmFileAction.rename:
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.drive_file_move_outlined,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
Text(translate('Rename'))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
|
||||
localPlatform = kPeerPlatformWindows;
|
||||
} else if (isLinux) {
|
||||
localPlatform = kPeerPlatformLinux;
|
||||
} else if (isWebOnWindows || isWebOnLinux) {
|
||||
localPlatform = kPeerPlatformWebDesktop;
|
||||
}
|
||||
// to-do: web desktop support ?
|
||||
return localPlatform;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: widget.tooltip,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
|
||||
@@ -38,18 +38,16 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
|
||||
@override
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
|
||||
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(enabled?.value);
|
||||
}
|
||||
|
||||
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
extends State<W> {
|
||||
RxBool enabled = true.obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.enabled != null) {
|
||||
enabled.value = widget.enabled!.value;
|
||||
MyPopupMenuItemState(bool? e) {
|
||||
if (e != null) {
|
||||
enabled.value = e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -372,7 +372,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
initState() {
|
||||
super.initState();
|
||||
|
||||
Future.delayed(Duration.zero, () async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
_fractionX.value = double.tryParse(await bind.sessionGetOption(
|
||||
sessionId: widget.ffi.sessionId,
|
||||
arg: 'remote-menubar-drag-x') ??
|
||||
@@ -452,8 +452,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
|
||||
Widget _buildToolbar(BuildContext context) {
|
||||
final List<Widget> toolbarItems = [];
|
||||
toolbarItems.add(_PinMenu(state: widget.state));
|
||||
if (!isWebDesktop) {
|
||||
toolbarItems.add(_PinMenu(state: widget.state));
|
||||
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
||||
}
|
||||
|
||||
@@ -1032,11 +1032,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
FFI get ffi => widget.ffi;
|
||||
String get id => widget.id;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_screenAdjustor.updateScreen();
|
||||
@@ -1278,7 +1273,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getLocalResolutionWayland();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_getLocalResolutionWayland();
|
||||
});
|
||||
}
|
||||
|
||||
Rect? scaledRect() {
|
||||
@@ -1615,7 +1612,9 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
// If use flutter to grab keys, we can only use one mode.
|
||||
// Map mode and Legacy mode, at least one of them is supported.
|
||||
String? modeOnly;
|
||||
if (isInputSourceFlutter) {
|
||||
// Keep both map and legacy mode on web at the moment.
|
||||
// TODO: Remove legacy mode after web supports translate mode on web.
|
||||
if (isInputSourceFlutter && isDesktop) {
|
||||
if (bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: ffi.sessionId, mode: kKeyMapMode)) {
|
||||
modeOnly = kKeyMapMode;
|
||||
@@ -1719,7 +1718,9 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, value);
|
||||
final viewOnly = await bind.sessionGetToggleOption(
|
||||
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
|
||||
ffiModel.setViewOnly(id, viewOnly ?? value);
|
||||
}
|
||||
: null,
|
||||
ffi: ffi,
|
||||
|
||||
@@ -227,8 +227,7 @@ typedef TabMenuBuilder = Widget Function(String key);
|
||||
typedef LabelGetter = Rx<String> Function(String key);
|
||||
|
||||
/// [_lastClickTime], help to handle double click
|
||||
int _lastClickTime =
|
||||
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
||||
int _lastClickTime = 0;
|
||||
|
||||
class DesktopTab extends StatefulWidget {
|
||||
final bool showLogo;
|
||||
@@ -506,17 +505,20 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
Obx(() {
|
||||
if (stateGlobal.showTabBar.isTrue &&
|
||||
!(kUseCompatibleUiMode && isHideSingleItem())) {
|
||||
final showBottomDivider = _showTabBarBottomDivider(tabType);
|
||||
return SizedBox(
|
||||
height: _kTabBarHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _kTabBarHeight - 1,
|
||||
height:
|
||||
showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight,
|
||||
child: _buildBar(),
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
if (showBottomDivider)
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -550,6 +552,13 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
controller: state.value.pageController,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: () {
|
||||
if (DesktopTabType.cm == tabType) {
|
||||
// Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful.
|
||||
return state.value.tabs.map((tab) {
|
||||
return tab.page;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// to-do refactor, separate connection state and UI state for remote session.
|
||||
/// [workaround] PageView children need an immutable list, after it has been passed into PageView
|
||||
final tabLen = state.value.tabs.length;
|
||||
@@ -727,16 +736,6 @@ class WindowActionPanel extends StatefulWidget {
|
||||
}
|
||||
|
||||
class WindowActionPanelState extends State<WindowActionPanel> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool showTabDowndown() {
|
||||
return widget.tabController.state.value.tabs.length > 1 &&
|
||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||
@@ -1172,7 +1171,10 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _kTabBarHeight,
|
||||
// _kTabBarHeight also displays normally
|
||||
height: _showTabBarBottomDivider(widget.tabType)
|
||||
? _kTabBarHeight - 1
|
||||
: _kTabBarHeight,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -1271,13 +1273,7 @@ class ActionIcon extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ActionIconState extends State<ActionIcon> {
|
||||
var hover = false.obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hover.value = false;
|
||||
}
|
||||
final hover = false.obs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1431,6 +1427,10 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _showTabBarBottomDivider(DesktopTabType tabType) {
|
||||
return tabType == DesktopTabType.main || tabType == DesktopTabType.install;
|
||||
}
|
||||
|
||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||
final Color? selectedTabIconColor;
|
||||
final Color? unSelectedTabIconColor;
|
||||
|
||||
@@ -260,7 +260,7 @@ showCmWindow({bool isStartup = false}) async {
|
||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
||||
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||
bind.mainHideDocker();
|
||||
bind.mainHideDock();
|
||||
await Future.wait([
|
||||
windowManager.show(),
|
||||
windowManager.focus(),
|
||||
@@ -288,14 +288,14 @@ hideCmWindow({bool isStartup = false}) async {
|
||||
size: kConnectionManagerWindowSizeClosedChat);
|
||||
windowManager.setOpacity(0);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||
bind.mainHideDocker();
|
||||
bind.mainHideDock();
|
||||
await windowManager.minimize();
|
||||
await windowManager.hide();
|
||||
_isCmReadyToShow = true;
|
||||
} else if (_isCmReadyToShow) {
|
||||
if (await windowManager.getOpacity() != 0) {
|
||||
await windowManager.setOpacity(0);
|
||||
bind.mainHideDocker();
|
||||
bind.mainHideDock();
|
||||
await windowManager.minimize();
|
||||
await windowManager.hide();
|
||||
}
|
||||
@@ -372,7 +372,7 @@ class App extends StatefulWidget {
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> {
|
||||
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -396,6 +396,34 @@ class _AppState extends State<App> {
|
||||
bind.mainChangeTheme(dark: to.toShortString());
|
||||
}
|
||||
};
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
_updateOrientation();
|
||||
}
|
||||
|
||||
void _updateOrientation() {
|
||||
if (isDesktop) return;
|
||||
|
||||
// Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
|
||||
// my test (Flutter 3.19.6, Android 14) is always the reverse value.
|
||||
// https://github.com/flutter/flutter/issues/60899
|
||||
// stateGlobal.isPortrait.value =
|
||||
// MediaQuery.of(context).orientation == Orientation.portrait;
|
||||
|
||||
final orientation = View.of(context).physicalSize.aspectRatio > 1
|
||||
? Orientation.landscape
|
||||
: Orientation.portrait;
|
||||
stateGlobal.isPortrait.value = orientation == Orientation.portrait;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,19 +9,16 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/autocomplete.dart';
|
||||
import '../../consts.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import 'home_page.dart';
|
||||
import 'scan_page.dart';
|
||||
import 'settings_page.dart';
|
||||
|
||||
/// Connection page for connecting to a remote peer.
|
||||
class ConnectionPage extends StatefulWidget implements PageShape {
|
||||
ConnectionPage({Key? key}) : super(key: key);
|
||||
ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
|
||||
|
||||
@override
|
||||
final icon = const Icon(Icons.connected_tv);
|
||||
@@ -30,7 +27,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
|
||||
final title = translate("Connection");
|
||||
|
||||
@override
|
||||
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
|
||||
final List<Widget> appBarActions;
|
||||
|
||||
@override
|
||||
State<ConnectionPage> createState() => _ConnectionPageState();
|
||||
@@ -50,19 +47,26 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
bool isPeersLoaded = false;
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
|
||||
_ConnectionPageState() {
|
||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||
_idController.addListener(() {
|
||||
_idEmpty.value = _idController.text.isEmpty;
|
||||
});
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!isWeb) _uniLinksSubscription = listenUniLinks();
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
if (lastRemoteId != _idController.id) {
|
||||
setState(() {
|
||||
_idController.id = lastRemoteId;
|
||||
});
|
||||
}
|
||||
}();
|
||||
});
|
||||
}
|
||||
if (isAndroid) {
|
||||
if (!bind.isCustomClient()) {
|
||||
@@ -72,11 +76,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_idController.addListener(() {
|
||||
_idEmpty.value = _idController.text.isEmpty;
|
||||
});
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -250,6 +249,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
),
|
||||
),
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
onSubmitted: (_) {
|
||||
onConnect();
|
||||
},
|
||||
);
|
||||
},
|
||||
onSelected: (option) {
|
||||
@@ -354,73 +356,3 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class WebMenu extends StatefulWidget {
|
||||
const WebMenu({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<WebMenu> createState() => _WebMenuState();
|
||||
}
|
||||
|
||||
class _WebMenuState extends State<WebMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
return PopupMenuButton<String>(
|
||||
tooltip: "",
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
return (isIOS
|
||||
? [
|
||||
const PopupMenuItem(
|
||||
value: "scan",
|
||||
child: Icon(Icons.qr_code_scanner, color: Colors.black),
|
||||
)
|
||||
]
|
||||
: <PopupMenuItem<String>>[]) +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "server",
|
||||
child: Text(translate('ID/Relay Server')),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "login",
|
||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate("Login")
|
||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "about",
|
||||
child: Text(translate('About RustDesk')),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 'server') {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'about') {
|
||||
showAbout(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'login') {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
}
|
||||
if (value == 'scan') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ScanPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,36 +204,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
setState(() {});
|
||||
} else if (v == "folder") {
|
||||
final name = TextEditingController();
|
||||
gFFI.dialogManager
|
||||
.show((setState, close, context) => CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate(
|
||||
"Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
String? errorText;
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
name.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
translate("Please enter the folder name"),
|
||||
errorText: errorText,
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel",
|
||||
onPressed: () => close(false),
|
||||
isOutline: true),
|
||||
dialogButton("OK", onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
currentFileController.createDir(
|
||||
PathUtil.join(
|
||||
currentDir.path,
|
||||
name.value.text,
|
||||
currentOptions.isWindows));
|
||||
close();
|
||||
}
|
||||
})
|
||||
]));
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel",
|
||||
onPressed: () => close(false), isOutline: true),
|
||||
dialogButton("OK", onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
if (!PathUtil.validName(
|
||||
name.value.text,
|
||||
currentFileController
|
||||
.options.value.isWindows)) {
|
||||
setState(() {
|
||||
errorText =
|
||||
translate("Invalid folder name");
|
||||
});
|
||||
return;
|
||||
}
|
||||
currentFileController.createDir(PathUtil.join(
|
||||
currentDir.path,
|
||||
name.value.text,
|
||||
currentOptions.isWindows));
|
||||
close();
|
||||
}
|
||||
})
|
||||
]);
|
||||
});
|
||||
} else if (v == "hidden") {
|
||||
currentFileController.toggleShowHidden();
|
||||
}
|
||||
@@ -497,7 +515,15 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
child: Text(translate("Properties")),
|
||||
value: "properties",
|
||||
enabled: false,
|
||||
)
|
||||
),
|
||||
if (!entries[index].isDrive &&
|
||||
versionCmp(gFFI.ffiModel.pi.version,
|
||||
"1.3.0") >=
|
||||
0)
|
||||
PopupMenuItem(
|
||||
child: Text(translate("Rename")),
|
||||
value: "rename",
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (v) {
|
||||
@@ -509,6 +535,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
_selectedItems.clear();
|
||||
widget.selectMode.toggle(isLocal);
|
||||
setState(() {});
|
||||
} else if (v == "rename") {
|
||||
controller.renameAction(
|
||||
entries[index], isLocal);
|
||||
}
|
||||
}),
|
||||
onTap: () {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/server_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:flutter_hbb/web/settings_page.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/chat_page.dart';
|
||||
@@ -45,7 +46,11 @@ class HomePageState extends State<HomePage> {
|
||||
|
||||
void initPages() {
|
||||
_pages.clear();
|
||||
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
|
||||
if (!bind.isIncomingOnly()) {
|
||||
_pages.add(ConnectionPage(
|
||||
appBarActions: [],
|
||||
));
|
||||
}
|
||||
if (isAndroid && !bind.isOutgoingOnly()) {
|
||||
_chatPageTabIndex = _pages.length;
|
||||
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
||||
@@ -149,7 +154,8 @@ class HomePageState extends State<HomePage> {
|
||||
}
|
||||
|
||||
class WebHomePage extends StatelessWidget {
|
||||
final connectionPage = ConnectionPage();
|
||||
final connectionPage =
|
||||
ConnectionPage(appBarActions: <Widget>[const WebSettingsPage()]);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -34,7 +34,7 @@ class RemotePage extends StatefulWidget {
|
||||
final bool? isSharedPassword;
|
||||
|
||||
@override
|
||||
State<RemotePage> createState() => _RemotePageState();
|
||||
State<RemotePage> createState() => _RemotePageState(id);
|
||||
}
|
||||
|
||||
class _RemotePageState extends State<RemotePage> {
|
||||
@@ -58,6 +58,12 @@ class _RemotePageState extends State<RemotePage> {
|
||||
final TextEditingController _textController =
|
||||
TextEditingController(text: initText);
|
||||
|
||||
_RemotePageState(String id) {
|
||||
initSharedStates(id);
|
||||
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
gFFI.dialogManager.loadMobileActionsOverlayVisible();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -80,12 +86,9 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
keyboardSubscription =
|
||||
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
||||
initSharedStates(widget.id);
|
||||
gFFI.chatModel
|
||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
_blockableOverlayState.applyFfi(gFFI);
|
||||
gFFI.dialogManager.loadMobileActionsOverlayVisible();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -776,11 +779,6 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
onPressed: onPressed);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
_updateRect() {
|
||||
RenderObject? renderObject = _key.currentContext?.findRenderObject();
|
||||
if (renderObject == null) {
|
||||
|
||||
@@ -187,7 +187,7 @@ class _ServerPageState extends State<ServerPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
buildPresetPasswordWarning(),
|
||||
buildPresetPasswordWarningMobile(),
|
||||
gFFI.serverModel.isStart
|
||||
? ServerInfo()
|
||||
: ServiceNotRunningNotification(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
@@ -87,12 +88,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _hideServer = false;
|
||||
var _hideProxy = false;
|
||||
var _hideNetwork = false;
|
||||
var _enableTrustedDevices = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_SettingsState() {
|
||||
_enableAbr = option2bool(
|
||||
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
||||
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
||||
@@ -117,8 +115,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||
_hideNetwork =
|
||||
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
||||
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
||||
}
|
||||
|
||||
() async {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
var update = false;
|
||||
|
||||
if (_hasIgnoreBattery) {
|
||||
@@ -177,7 +182,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
if (update) {
|
||||
setState(() {});
|
||||
}
|
||||
}();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -241,18 +246,76 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
],
|
||||
));
|
||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
final enable2fa = bind.mainHasValid2FaSync();
|
||||
final List<AbstractSettingsTile> tfaTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('enable-2fa-title')),
|
||||
initialValue: bind.mainHasValid2FaSync(),
|
||||
onToggle: (_) async {
|
||||
initialValue: enable2fa,
|
||||
onToggle: (v) async {
|
||||
update() async {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
change2fa(callback: update);
|
||||
if (v == false) {
|
||||
CommonConfirmDialog(
|
||||
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
||||
change2fa(callback: update);
|
||||
});
|
||||
} else {
|
||||
change2fa(callback: update);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (enable2fa)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Telegram bot')),
|
||||
initialValue: bind.mainHasValidBotSync(),
|
||||
onToggle: (v) async {
|
||||
update() async {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
if (v == false) {
|
||||
CommonConfirmDialog(
|
||||
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
|
||||
changeBot(callback: update);
|
||||
});
|
||||
} else {
|
||||
changeBot(callback: update);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (enable2fa)
|
||||
SettingsTile.switchTile(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate('Enable trusted devices')),
|
||||
Text('* ${translate('enable-trusted-devices-tip')}',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
initialValue: _enableTrustedDevices,
|
||||
onToggle: isOptionFixed(kOptionEnableTrustedDevices)
|
||||
? null
|
||||
: (v) async {
|
||||
mainSetBoolOption(kOptionEnableTrustedDevices, v);
|
||||
setState(() {
|
||||
_enableTrustedDevices = v;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (enable2fa && _enableTrustedDevices)
|
||||
SettingsTile(
|
||||
title: Text(translate('Manage trusted devices')),
|
||||
trailing: Icon(Icons.arrow_forward_ios),
|
||||
onPressed: (context) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return _ManageTrustedDevices();
|
||||
}));
|
||||
})
|
||||
];
|
||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Deny LAN discovery')),
|
||||
initialValue: _denyLANDiscovery,
|
||||
@@ -640,6 +703,11 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isAndroid &&
|
||||
!disabledSettings &&
|
||||
!outgoingOnly &&
|
||||
!hideSecuritySettings)
|
||||
SettingsSection(title: Text('2FA'), tiles: tfaTiles),
|
||||
if (isAndroid &&
|
||||
!disabledSettings &&
|
||||
!outgoingOnly &&
|
||||
@@ -961,6 +1029,51 @@ class __DisplayPageState extends State<_DisplayPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ManageTrustedDevices extends StatefulWidget {
|
||||
const _ManageTrustedDevices();
|
||||
|
||||
@override
|
||||
State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState();
|
||||
}
|
||||
|
||||
class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> {
|
||||
RxList<TrustedDevice> trustedDevices = RxList.empty(growable: true);
|
||||
RxList<Uint8List> selectedDevices = RxList.empty();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(translate('Manage trusted devices')),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
Obx(() => IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.white),
|
||||
onPressed: selectedDevices.isEmpty
|
||||
? null
|
||||
: () {
|
||||
confrimDeleteTrustedDevicesDialog(
|
||||
trustedDevices, selectedDevices);
|
||||
}))
|
||||
],
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: TrustedDevice.get(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('Error: ${snapshot.error}'));
|
||||
}
|
||||
final devices = snapshot.data as List<TrustedDevice>;
|
||||
trustedDevices = devices.obs;
|
||||
return trustedDevicesTable(trustedDevices, selectedDevices);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioEntry {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@@ -41,18 +41,16 @@ class GestureHelp extends StatefulWidget {
|
||||
final OnTouchModeChange onTouchModeChange;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GestureHelpState();
|
||||
State<StatefulWidget> createState() => _GestureHelpState(touchMode);
|
||||
}
|
||||
|
||||
class _GestureHelpState extends State<GestureHelp> {
|
||||
var _selectedIndex;
|
||||
var _touchMode;
|
||||
late int _selectedIndex;
|
||||
late bool _touchMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_touchMode = widget.touchMode;
|
||||
_GestureHelpState(bool touchMode) {
|
||||
_touchMode = touchMode;
|
||||
_selectedIndex = _touchMode ? 1 : 0;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,17 +17,17 @@ import '../common.dart';
|
||||
|
||||
final syncAbOption = 'sync-ab-with-recent-sessions';
|
||||
bool shouldSyncAb() {
|
||||
return bind.mainGetLocalOption(key: syncAbOption).isNotEmpty;
|
||||
return bind.mainGetLocalOption(key: syncAbOption) == 'Y';
|
||||
}
|
||||
|
||||
final sortAbTagsOption = 'sync-ab-tags';
|
||||
bool shouldSortTags() {
|
||||
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
|
||||
return bind.mainGetLocalOption(key: sortAbTagsOption) == 'Y';
|
||||
}
|
||||
|
||||
final filterAbTagOption = 'filter-ab-by-intersection';
|
||||
bool filterAbTagByIntersection() {
|
||||
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty;
|
||||
return bind.mainGetLocalOption(key: filterAbTagOption) == 'Y';
|
||||
}
|
||||
|
||||
const _personalAddressBookName = "My address book";
|
||||
@@ -111,9 +111,10 @@ class AbModel {
|
||||
Future<void> _pullAb(
|
||||
{required ForcePullAb? force, required bool quiet}) async {
|
||||
if (bind.isDisableAb()) return;
|
||||
debugPrint("pullAb, force: $force, quiet: $quiet");
|
||||
if (!gFFI.userModel.isLogin) return;
|
||||
if (gFFI.userModel.networkError.isNotEmpty) return;
|
||||
if (force == null && listInitialized && current.initialized) return;
|
||||
debugPrint("pullAb, force: $force, quiet: $quiet");
|
||||
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
|
||||
try {
|
||||
// Read personal guid every time to avoid upgrading the server without closing the main window
|
||||
@@ -815,8 +816,6 @@ abstract class BaseAb {
|
||||
}
|
||||
|
||||
class LegacyAb extends BaseAb {
|
||||
final sortTags = shouldSortTags().obs;
|
||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||
// licensedDevices is obtained from personal ab, shared ab restrict it in server
|
||||
var licensedDevices = 0;
|
||||
@@ -1209,8 +1208,6 @@ class LegacyAb extends BaseAb {
|
||||
class Ab extends BaseAb {
|
||||
AbProfile profile;
|
||||
late final bool personal;
|
||||
final sortTags = shouldSortTags().obs;
|
||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
||||
bool get emtpy => peers.isEmpty && tags.isEmpty;
|
||||
|
||||
Ab(this.profile, this.personal);
|
||||
|
||||
@@ -33,6 +33,8 @@ class CmFileModel {
|
||||
_onFileRemove(evt['remove']);
|
||||
} else if (evt['create_dir'] != null) {
|
||||
_onDirCreate(evt['create_dir']);
|
||||
} else if (evt['rename'] != null) {
|
||||
_onRename(evt['rename']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +61,6 @@ class CmFileModel {
|
||||
|
||||
_dealOneJob(dynamic l, bool calcSpeed) {
|
||||
final data = TransferJobSerdeData.fromJson(l);
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
@@ -70,12 +70,7 @@ class CmFileModel {
|
||||
if (job == null) {
|
||||
job = CmFileLog();
|
||||
jobTable.add(job);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == data.connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
_addUnread(data.connId);
|
||||
}
|
||||
job.id = data.id;
|
||||
job.action =
|
||||
@@ -167,8 +162,6 @@ class CmFileModel {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
FileActionLog data = FileActionLog.fromJson(d);
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
@@ -179,17 +172,45 @@ class CmFileModel {
|
||||
..fileName = data.path
|
||||
..action = CmFileAction.createDir
|
||||
..state = JobState.done);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == data.connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
_addUnread(data.connId);
|
||||
jobTable.refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
_onRename(dynamic log) {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
FileRenamenLog data = FileRenamenLog.fromJson(d);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
return;
|
||||
}
|
||||
final fileName = '${data.path} -> ${data.newName}';
|
||||
jobTable.add(CmFileLog()
|
||||
..id = 0
|
||||
..fileName = fileName
|
||||
..action = CmFileAction.rename
|
||||
..state = JobState.done);
|
||||
_addUnread(data.connId);
|
||||
jobTable.refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
_addUnread(int connId) {
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CmFileAction {
|
||||
@@ -198,6 +219,7 @@ enum CmFileAction {
|
||||
localToRemote,
|
||||
remove,
|
||||
createDir,
|
||||
rename,
|
||||
}
|
||||
|
||||
class CmFileLog {
|
||||
@@ -285,3 +307,22 @@ class FileActionLog {
|
||||
dir: d['dir'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
class FileRenamenLog {
|
||||
int connId = 0;
|
||||
String path = '';
|
||||
String newName = '';
|
||||
|
||||
FileRenamenLog({
|
||||
required this.connId,
|
||||
required this.path,
|
||||
required this.newName,
|
||||
});
|
||||
|
||||
FileRenamenLog.fromJson(dynamic d)
|
||||
: this(
|
||||
connId: d['connId'] ?? 0,
|
||||
path: d['path'] ?? '',
|
||||
newName: d['newName'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@ class TextureModel {
|
||||
}
|
||||
|
||||
updateCurrentDisplay(int curDisplay) {
|
||||
if (isWeb) return;
|
||||
final ffi = parent.target;
|
||||
if (ffi == null) return;
|
||||
tryCreateTexture(int idx) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/utils/event_loop.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
@@ -33,6 +34,7 @@ class JobID {
|
||||
}
|
||||
|
||||
typedef GetSessionID = SessionID Function();
|
||||
typedef GetDialogManager = OverlayDialogManager? Function();
|
||||
|
||||
class FileModel {
|
||||
final WeakReference<FFI> parent;
|
||||
@@ -44,13 +46,15 @@ class FileModel {
|
||||
late final FileController remoteController;
|
||||
|
||||
late final GetSessionID getSessionID;
|
||||
late final GetDialogManager getDialogManager;
|
||||
SessionID get sessionId => getSessionID();
|
||||
late final FileDialogEventLoop evtLoop;
|
||||
|
||||
FileModel(this.parent) {
|
||||
getSessionID = () => parent.target!.sessionId;
|
||||
getDialogManager = () => parent.target?.dialogManager;
|
||||
fileFetcher = FileFetcher(getSessionID);
|
||||
jobController = JobController(getSessionID);
|
||||
jobController = JobController(getSessionID, getDialogManager);
|
||||
localController = FileController(
|
||||
isLocal: true,
|
||||
getSessionID: getSessionID,
|
||||
@@ -450,7 +454,7 @@ class FileController {
|
||||
final isWindows = otherSideData.options.isWindows;
|
||||
final showHidden = otherSideData.options.showHidden;
|
||||
for (var from in items.items) {
|
||||
final jobID = jobController.add(from, isRemoteToLocal);
|
||||
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
|
||||
bind.sessionSendFiles(
|
||||
sessionId: sessionId,
|
||||
actId: jobID,
|
||||
@@ -493,13 +497,21 @@ class FileController {
|
||||
fd.format(isWindows);
|
||||
dialogManager?.dismissAll();
|
||||
if (fd.entries.isEmpty) {
|
||||
var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
|
||||
final confirm = await showRemoveDialog(
|
||||
translate(
|
||||
"Are you sure you want to delete this empty directory?"),
|
||||
item.name,
|
||||
false);
|
||||
if (confirm == true) {
|
||||
sendRemoveEmptyDir(item.path, 0);
|
||||
sendRemoveEmptyDir(
|
||||
item.path,
|
||||
0,
|
||||
deleteJobId,
|
||||
);
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
error: "cancel", state: JobState.done);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -507,6 +519,13 @@ class FileController {
|
||||
} else {
|
||||
entries = [];
|
||||
}
|
||||
int deleteJobId;
|
||||
if (item.isDirectory) {
|
||||
deleteJobId =
|
||||
jobController.addDeleteDirJob(item, !isLocal, entries.length);
|
||||
} else {
|
||||
deleteJobId = jobController.addDeleteFileJob(item, !isLocal);
|
||||
}
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
final dirShow = item.isDirectory
|
||||
@@ -521,24 +540,32 @@ class FileController {
|
||||
);
|
||||
try {
|
||||
if (confirm == true) {
|
||||
sendRemoveFile(entries[i].path, i);
|
||||
sendRemoveFile(entries[i].path, i, deleteJobId);
|
||||
final res = await jobController.jobResultListener.start();
|
||||
// handle remove res;
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i);
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
file_num: i, error: "cancel");
|
||||
}
|
||||
if (_removeCheckboxRemember) {
|
||||
if (confirm == true) {
|
||||
for (var j = i + 1; j < entries.length; j++) {
|
||||
sendRemoveFile(entries[j].path, j);
|
||||
sendRemoveFile(entries[j].path, j, deleteJobId);
|
||||
final res = await jobController.jobResultListener.start();
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i);
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
error: "cancel",
|
||||
file_num: entries.length,
|
||||
state: JobState.done);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -617,22 +644,19 @@ class FileController {
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
void sendRemoveFile(String path, int fileNum) {
|
||||
void sendRemoveFile(String path, int fileNum, int actId) {
|
||||
bind.sessionRemoveFile(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
actId: actId,
|
||||
path: path,
|
||||
isRemote: !isLocal,
|
||||
fileNum: fileNum);
|
||||
}
|
||||
|
||||
void sendRemoveEmptyDir(String path, int fileNum) {
|
||||
void sendRemoveEmptyDir(String path, int fileNum, int actId) {
|
||||
history.removeWhere((element) => element.contains(path));
|
||||
bind.sessionRemoveAllEmptyDirs(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
path: path,
|
||||
isRemote: !isLocal);
|
||||
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
|
||||
}
|
||||
|
||||
Future<void> createDir(String path) async {
|
||||
@@ -642,29 +666,102 @@ class FileController {
|
||||
path: path,
|
||||
isRemote: !isLocal);
|
||||
}
|
||||
|
||||
Future<void> renameAction(Entry item, bool isLocal) async {
|
||||
final textEditingController = TextEditingController(text: item.name);
|
||||
String? errorText;
|
||||
dialogManager?.show((setState, close, context) {
|
||||
textEditingController.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
submit() async {
|
||||
final newName = textEditingController.text;
|
||||
if (newName.isEmpty || newName == item.name) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (directory.value.entries.any((e) => e.name == newName)) {
|
||||
setState(() {
|
||||
errorText = translate("Already exists");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!PathUtil.validName(newName, options.value.isWindows)) {
|
||||
setState(() {
|
||||
if (item.isDirectory) {
|
||||
errorText = translate("Invalid folder name");
|
||||
} else {
|
||||
errorText = translate("Invalid file name");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
await bind.sessionRenameFile(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
path: item.path,
|
||||
newName: newName,
|
||||
isRemote: !isLocal);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Column(
|
||||
children: [
|
||||
DialogTextField(
|
||||
title: '${translate('Rename')} ${item.name}',
|
||||
controller: textEditingController,
|
||||
errorText: errorText,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const _kOneWayFileTransferError = 'one-way-file-transfer-tip';
|
||||
|
||||
class JobController {
|
||||
static final JobID jobID = JobID();
|
||||
final jobTable = List<JobProgress>.empty(growable: true).obs;
|
||||
final jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||
final GetSessionID getSessionID;
|
||||
final GetDialogManager getDialogManager;
|
||||
SessionID get sessionId => getSessionID();
|
||||
OverlayDialogManager? get alogManager => getDialogManager();
|
||||
int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
JobController(this.getSessionID);
|
||||
JobController(this.getSessionID, this.getDialogManager);
|
||||
|
||||
int getJob(int id) {
|
||||
return jobTable.indexWhere((element) => element.id == id);
|
||||
}
|
||||
|
||||
// JobProgress? getJob(int id) {
|
||||
// return jobTable.firstWhere((element) => element.id == id);
|
||||
// }
|
||||
|
||||
// return jobID
|
||||
int add(Entry from, bool isRemoteToLocal) {
|
||||
int addTransferJob(Entry from, bool isRemoteToLocal) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = path.basename(from.path)
|
||||
..jobName = from.path
|
||||
..totalSize = from.size
|
||||
@@ -674,6 +771,33 @@ class JobController {
|
||||
return jobID;
|
||||
}
|
||||
|
||||
int addDeleteFileJob(Entry file, bool isRemote) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.deleteFile
|
||||
..fileName = path.basename(file.path)
|
||||
..jobName = file.path
|
||||
..totalSize = file.size
|
||||
..state = JobState.none
|
||||
..id = jobID
|
||||
..isRemoteToLocal = isRemote);
|
||||
return jobID;
|
||||
}
|
||||
|
||||
int addDeleteDirJob(Entry file, bool isRemote, int fileCount) {
|
||||
final jobID = JobController.jobID.next();
|
||||
jobTable.add(JobProgress()
|
||||
..type = JobType.deleteDir
|
||||
..fileName = path.basename(file.path)
|
||||
..jobName = file.path
|
||||
..fileCount = fileCount
|
||||
..totalSize = file.size
|
||||
..state = JobState.none
|
||||
..id = jobID
|
||||
..isRemoteToLocal = isRemote);
|
||||
return jobID;
|
||||
}
|
||||
|
||||
void tryUpdateJobProgress(Map<String, dynamic> evt) {
|
||||
try {
|
||||
int id = int.parse(evt['id']);
|
||||
@@ -684,6 +808,7 @@ class JobController {
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
job.speed = double.parse(evt['speed']);
|
||||
job.finishedSize = int.parse(evt['finished_size']);
|
||||
job.recvJobRes = true;
|
||||
debugPrint("update job $id with $evt");
|
||||
jobTable.refresh();
|
||||
}
|
||||
@@ -692,20 +817,48 @@ class JobController {
|
||||
}
|
||||
}
|
||||
|
||||
void jobDone(Map<String, dynamic> evt) async {
|
||||
Future<bool> jobDone(Map<String, dynamic> evt) async {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
return;
|
||||
// return;
|
||||
}
|
||||
|
||||
int id = int.parse(evt['id']);
|
||||
int id = -1;
|
||||
int? fileNum = 0;
|
||||
double? speed = 0;
|
||||
try {
|
||||
id = int.parse(evt['id']);
|
||||
} catch (_) {}
|
||||
final jobIndex = getJob(id);
|
||||
if (jobIndex != -1) {
|
||||
final job = jobTable[jobIndex];
|
||||
job.finishedSize = job.totalSize;
|
||||
if (jobIndex == -1) return true;
|
||||
final job = jobTable[jobIndex];
|
||||
job.recvJobRes = true;
|
||||
if (job.type == JobType.deleteFile) {
|
||||
job.state = JobState.done;
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
jobTable.refresh();
|
||||
} else if (job.type == JobType.deleteDir) {
|
||||
try {
|
||||
fileNum = int.tryParse(evt['file_num']);
|
||||
} catch (_) {}
|
||||
if (fileNum != null) {
|
||||
if (fileNum < job.fileNum) return true; // file_num can be 0 at last
|
||||
job.fileNum = fileNum;
|
||||
if (fileNum >= job.fileCount - 1) {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fileNum = int.tryParse(evt['file_num']);
|
||||
speed = double.tryParse(evt['speed']);
|
||||
} catch (_) {}
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
if (speed != null) job.speed = speed;
|
||||
job.state = JobState.done;
|
||||
}
|
||||
jobTable.refresh();
|
||||
if (job.type == JobType.deleteDir) {
|
||||
return job.state == JobState.done;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,16 +869,61 @@ class JobController {
|
||||
final job = jobTable[jobIndex];
|
||||
job.state = JobState.error;
|
||||
job.err = err;
|
||||
job.fileNum = int.parse(evt['file_num']);
|
||||
if (err == "skipped") {
|
||||
job.state = JobState.done;
|
||||
job.finishedSize = job.totalSize;
|
||||
job.recvJobRes = true;
|
||||
if (job.type == JobType.transfer) {
|
||||
int? fileNum = int.tryParse(evt['file_num']);
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
if (err == "skipped") {
|
||||
job.state = JobState.done;
|
||||
job.finishedSize = job.totalSize;
|
||||
}
|
||||
} else if (job.type == JobType.deleteDir) {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
}
|
||||
int? fileNum = int.tryParse(evt['file_num']);
|
||||
if (fileNum != null) job.fileNum = fileNum;
|
||||
} else if (job.type == JobType.deleteFile) {
|
||||
if (jobResultListener.isListening) {
|
||||
jobResultListener.complete(evt);
|
||||
}
|
||||
}
|
||||
jobTable.refresh();
|
||||
}
|
||||
if (err == _kOneWayFileTransferError) {
|
||||
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
|
||||
final dm = alogManager;
|
||||
if (dm != null) {
|
||||
_lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
|
||||
msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm);
|
||||
}
|
||||
}
|
||||
}
|
||||
debugPrint("jobError $evt");
|
||||
}
|
||||
|
||||
void updateJobStatus(int id,
|
||||
{int? file_num, String? error, JobState? state}) {
|
||||
final jobIndex = getJob(id);
|
||||
if (jobIndex < 0) return;
|
||||
final job = jobTable[jobIndex];
|
||||
job.recvJobRes = true;
|
||||
if (file_num != null) {
|
||||
job.fileNum = file_num;
|
||||
}
|
||||
if (error != null) {
|
||||
job.err = error;
|
||||
job.state = JobState.error;
|
||||
}
|
||||
if (state != null) {
|
||||
job.state = state;
|
||||
}
|
||||
if (job.type == JobType.deleteFile && error == null) {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
jobTable.refresh();
|
||||
}
|
||||
|
||||
Future<void> cancelJob(int id) async {
|
||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||
}
|
||||
@@ -742,6 +940,7 @@ class JobController {
|
||||
final currJobId = JobController.jobID.next();
|
||||
String fileName = path.basename(isRemote ? remote : to);
|
||||
var jobProgress = JobProgress()
|
||||
..type = JobType.transfer
|
||||
..fileName = fileName
|
||||
..jobName = isRemote ? remote : to
|
||||
..id = currJobId
|
||||
@@ -1016,8 +1215,12 @@ extension JobStateDisplay on JobState {
|
||||
}
|
||||
}
|
||||
|
||||
enum JobType { none, transfer, deleteFile, deleteDir }
|
||||
|
||||
class JobProgress {
|
||||
JobType type = JobType.none;
|
||||
JobState state = JobState.none;
|
||||
var recvJobRes = false;
|
||||
var id = 0;
|
||||
var fileNum = 0;
|
||||
var speed = 0.0;
|
||||
@@ -1037,7 +1240,9 @@ class JobProgress {
|
||||
int lastTransferredSize = 0;
|
||||
|
||||
clear() {
|
||||
type = JobType.none;
|
||||
state = JobState.none;
|
||||
recvJobRes = false;
|
||||
id = 0;
|
||||
fileNum = 0;
|
||||
speed = 0;
|
||||
@@ -1051,11 +1256,81 @@ class JobProgress {
|
||||
}
|
||||
|
||||
String display() {
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
if (type == JobType.transfer) {
|
||||
if (state == JobState.done && err == "skipped") {
|
||||
return translate("Skipped");
|
||||
}
|
||||
} else if (type == JobType.deleteFile) {
|
||||
if (err == "cancel") {
|
||||
return translate("Cancel");
|
||||
}
|
||||
}
|
||||
|
||||
return state.display();
|
||||
}
|
||||
|
||||
String getStatus() {
|
||||
int handledFileCount = recvJobRes ? fileNum + 1 : fileNum;
|
||||
if (handledFileCount >= fileCount) {
|
||||
handledFileCount = fileCount;
|
||||
}
|
||||
if (state == JobState.done) {
|
||||
handledFileCount = fileCount;
|
||||
finishedSize = totalSize;
|
||||
}
|
||||
final filesStr = "$handledFileCount/$fileCount files";
|
||||
final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : "";
|
||||
final sizePercentStr = totalSize > 0 && finishedSize > 0
|
||||
? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}"
|
||||
: "";
|
||||
if (type == JobType.deleteFile) {
|
||||
return display();
|
||||
} else if (type == JobType.deleteDir) {
|
||||
var res = '';
|
||||
if (state == JobState.done || state == JobState.error) {
|
||||
res = display();
|
||||
}
|
||||
if (filesStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += " ";
|
||||
}
|
||||
res += filesStr;
|
||||
}
|
||||
|
||||
if (sizeStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizeStr;
|
||||
}
|
||||
return res;
|
||||
} else if (type == JobType.transfer) {
|
||||
var res = "";
|
||||
if (state != JobState.inProgress && state != JobState.none) {
|
||||
res += display();
|
||||
}
|
||||
if (filesStr.isNotEmpty) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += filesStr;
|
||||
}
|
||||
if (sizeStr.isNotEmpty && state != JobState.inProgress) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizeStr;
|
||||
}
|
||||
if (sizePercentStr.isNotEmpty && state == JobState.inProgress) {
|
||||
if (res.isNotEmpty) {
|
||||
res += ", ";
|
||||
}
|
||||
res += sizePercentStr;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
class _PathStat {
|
||||
@@ -1083,6 +1358,13 @@ class PathUtil {
|
||||
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||
return pathUtil.dirname(path);
|
||||
}
|
||||
|
||||
static bool validName(String name, bool isWindows) {
|
||||
final unixFileNamePattern = RegExp(r'^[^/\0]+$');
|
||||
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
|
||||
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
|
||||
return reg.hasMatch(name);
|
||||
}
|
||||
}
|
||||
|
||||
class DirectoryOptions {
|
||||
|
||||
@@ -28,6 +28,7 @@ class GroupModel {
|
||||
Future<void> pull({force = true, quiet = false}) async {
|
||||
if (bind.isDisableGroupPanel()) return;
|
||||
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
||||
if (gFFI.userModel.networkError.isNotEmpty) return;
|
||||
if (!force && initialized) return;
|
||||
if (!quiet) {
|
||||
groupLoading.value = true;
|
||||
|
||||
@@ -177,7 +177,7 @@ class PointerEventToRust {
|
||||
}
|
||||
}
|
||||
|
||||
class ToReleaseKeys {
|
||||
class ToReleaseRawKeys {
|
||||
RawKeyEvent? lastLShiftKeyEvent;
|
||||
RawKeyEvent? lastRShiftKeyEvent;
|
||||
RawKeyEvent? lastLCtrlKeyEvent;
|
||||
@@ -282,6 +282,48 @@ class ToReleaseKeys {
|
||||
}
|
||||
}
|
||||
|
||||
class ToReleaseKeys {
|
||||
KeyEvent? lastLShiftKeyEvent;
|
||||
KeyEvent? lastRShiftKeyEvent;
|
||||
KeyEvent? lastLCtrlKeyEvent;
|
||||
KeyEvent? lastRCtrlKeyEvent;
|
||||
KeyEvent? lastLAltKeyEvent;
|
||||
KeyEvent? lastRAltKeyEvent;
|
||||
KeyEvent? lastLCommandKeyEvent;
|
||||
KeyEvent? lastRCommandKeyEvent;
|
||||
KeyEvent? lastSuperKeyEvent;
|
||||
|
||||
reset() {
|
||||
lastLShiftKeyEvent = null;
|
||||
lastRShiftKeyEvent = null;
|
||||
lastLCtrlKeyEvent = null;
|
||||
lastRCtrlKeyEvent = null;
|
||||
lastLAltKeyEvent = null;
|
||||
lastRAltKeyEvent = null;
|
||||
lastLCommandKeyEvent = null;
|
||||
lastRCommandKeyEvent = null;
|
||||
lastSuperKeyEvent = null;
|
||||
}
|
||||
|
||||
release(KeyEventResult Function(KeyEvent e) handleKeyEvent) {
|
||||
for (final key in [
|
||||
lastLShiftKeyEvent,
|
||||
lastRShiftKeyEvent,
|
||||
lastLCtrlKeyEvent,
|
||||
lastRCtrlKeyEvent,
|
||||
lastLAltKeyEvent,
|
||||
lastRAltKeyEvent,
|
||||
lastLCommandKeyEvent,
|
||||
lastRCommandKeyEvent,
|
||||
lastSuperKeyEvent,
|
||||
]) {
|
||||
if (key != null) {
|
||||
handleKeyEvent(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InputModel {
|
||||
final WeakReference<FFI> parent;
|
||||
String keyboardMode = '';
|
||||
@@ -292,6 +334,7 @@ class InputModel {
|
||||
var alt = false;
|
||||
var command = false;
|
||||
|
||||
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
|
||||
final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
|
||||
|
||||
// trackpad
|
||||
@@ -339,6 +382,91 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
void handleKeyDownEventModifiers(KeyEvent e) {
|
||||
KeyUpEvent upEvent(e) => KeyUpEvent(
|
||||
physicalKey: e.physicalKey,
|
||||
logicalKey: e.logicalKey,
|
||||
timeStamp: e.timeStamp,
|
||||
);
|
||||
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
|
||||
if (!alt) {
|
||||
alt = true;
|
||||
}
|
||||
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
|
||||
if (!alt) {
|
||||
alt = true;
|
||||
}
|
||||
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
|
||||
if (!ctrl) {
|
||||
ctrl = true;
|
||||
}
|
||||
toReleaseKeys.lastLCtrlKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
|
||||
if (!ctrl) {
|
||||
ctrl = true;
|
||||
}
|
||||
toReleaseKeys.lastRCtrlKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
|
||||
if (!shift) {
|
||||
shift = true;
|
||||
}
|
||||
toReleaseKeys.lastLShiftKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
|
||||
if (!shift) {
|
||||
shift = true;
|
||||
}
|
||||
toReleaseKeys.lastRShiftKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
|
||||
if (!command) {
|
||||
command = true;
|
||||
}
|
||||
toReleaseKeys.lastLCommandKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
|
||||
if (!command) {
|
||||
command = true;
|
||||
}
|
||||
toReleaseKeys.lastRCommandKeyEvent = upEvent(e);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
|
||||
if (!command) {
|
||||
command = true;
|
||||
}
|
||||
toReleaseKeys.lastSuperKeyEvent = upEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void handleKeyUpEventModifiers(KeyEvent e) {
|
||||
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
|
||||
alt = false;
|
||||
toReleaseKeys.lastLAltKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
|
||||
alt = false;
|
||||
toReleaseKeys.lastRAltKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
|
||||
ctrl = false;
|
||||
toReleaseKeys.lastLCtrlKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
|
||||
ctrl = false;
|
||||
toReleaseKeys.lastRCtrlKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
|
||||
shift = false;
|
||||
toReleaseKeys.lastLShiftKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
|
||||
shift = false;
|
||||
toReleaseKeys.lastRShiftKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
|
||||
command = false;
|
||||
toReleaseKeys.lastLCommandKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
|
||||
command = false;
|
||||
toReleaseKeys.lastRCommandKeyEvent = null;
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
|
||||
command = false;
|
||||
toReleaseKeys.lastSuperKeyEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
||||
@@ -358,7 +486,7 @@ class InputModel {
|
||||
command = true;
|
||||
}
|
||||
}
|
||||
toReleaseKeys.updateKeyDown(key, e);
|
||||
toReleaseRawKeys.updateKeyDown(key, e);
|
||||
}
|
||||
if (e is RawKeyUpEvent) {
|
||||
if (key == LogicalKeyboardKey.altLeft ||
|
||||
@@ -376,12 +504,46 @@ class InputModel {
|
||||
command = false;
|
||||
}
|
||||
|
||||
toReleaseKeys.updateKeyUp(key, e);
|
||||
toReleaseRawKeys.updateKeyUp(key, e);
|
||||
}
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == 'map') {
|
||||
mapKeyboardMode(e);
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
mapKeyboardModeRaw(e);
|
||||
} else {
|
||||
legacyKeyboardModeRaw(e);
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
KeyEventResult handleKeyEvent(KeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (isWindows || isLinux) {
|
||||
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
|
||||
if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||
|
||||
e.physicalKey == PhysicalKeyboardKey.metaRight) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
if (e is KeyUpEvent) {
|
||||
handleKeyUpEventModifiers(e);
|
||||
} else if (e is KeyDownEvent) {
|
||||
handleKeyDownEventModifiers(e);
|
||||
}
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
// FIXME: e.character is wrong for dead keys, eg: ^ in de
|
||||
newKeyboardMode(
|
||||
e.character ?? '',
|
||||
e.physicalKey.usbHidUsage & 0xFFFF,
|
||||
// Show repeat event be converted to "release+press" events?
|
||||
e is KeyDownEvent || e is KeyRepeatEvent);
|
||||
} else {
|
||||
legacyKeyboardMode(e);
|
||||
}
|
||||
@@ -389,7 +551,33 @@ class InputModel {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
void mapKeyboardMode(RawKeyEvent e) {
|
||||
/// Send Key Event
|
||||
void newKeyboardMode(String character, int usbHid, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.capsLock)) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.numLock)) {
|
||||
lockModes |= (1 << numlock);
|
||||
}
|
||||
if (HardwareKeyboard.instance.lockModesEnabled
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
sessionId: sessionId,
|
||||
character: character,
|
||||
usbHid: usbHid,
|
||||
lockModes: lockModes,
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
void mapKeyboardModeRaw(RawKeyEvent e) {
|
||||
int positionCode = -1;
|
||||
int platformCode = -1;
|
||||
bool down;
|
||||
@@ -441,7 +629,7 @@ class InputModel {
|
||||
.contains(KeyboardLockMode.scrollLock)) {
|
||||
lockModes |= (1 << scrolllock);
|
||||
}
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
bind.sessionHandleFlutterRawKeyEvent(
|
||||
sessionId: sessionId,
|
||||
name: name,
|
||||
platformCode: platformCode,
|
||||
@@ -450,7 +638,7 @@ class InputModel {
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
void legacyKeyboardMode(RawKeyEvent e) {
|
||||
void legacyKeyboardModeRaw(RawKeyEvent e) {
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (e.repeat) {
|
||||
sendRawKey(e, press: true);
|
||||
@@ -471,6 +659,24 @@ class InputModel {
|
||||
inputKey(label, down: down, press: press ?? false);
|
||||
}
|
||||
|
||||
void legacyKeyboardMode(KeyEvent e) {
|
||||
if (e is KeyDownEvent) {
|
||||
sendKey(e, down: true);
|
||||
} else if (e is KeyRepeatEvent) {
|
||||
sendKey(e, press: true);
|
||||
} else if (e is KeyUpEvent) {
|
||||
sendKey(e);
|
||||
}
|
||||
}
|
||||
|
||||
void sendKey(KeyEvent e, {bool? down, bool? press}) {
|
||||
// for maximum compatibility
|
||||
final label = physicalKeyMap[e.physicalKey.usbHidUsage] ??
|
||||
logicalKeyMap[e.logicalKey.keyId] ??
|
||||
e.logicalKey.keyLabel;
|
||||
inputKey(label, down: down, press: press ?? false);
|
||||
}
|
||||
|
||||
/// Send key stroke event.
|
||||
/// [down] indicates the key's state(down or up).
|
||||
/// [press] indicates a click event(down and up).
|
||||
@@ -566,7 +772,8 @@ class InputModel {
|
||||
}
|
||||
|
||||
void enterOrLeave(bool enter) {
|
||||
toReleaseKeys.release(handleRawKeyEvent);
|
||||
toReleaseKeys.release(handleKeyEvent);
|
||||
toReleaseRawKeys.release(handleRawKeyEvent);
|
||||
_pointerMovedAfterEnter = false;
|
||||
|
||||
// Fix status
|
||||
@@ -577,6 +784,9 @@ class InputModel {
|
||||
if (!isInputSourceFlutter) {
|
||||
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
|
||||
}
|
||||
if (!isWeb && enter) {
|
||||
bind.setCurSessionId(sessionId: sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send mouse movement event with distance in [x] and [y].
|
||||
@@ -1164,15 +1374,15 @@ class InputModel {
|
||||
// Simulate a key press event.
|
||||
// `usbHidUsage` is the USB HID usage code of the key.
|
||||
Future<void> tapHidKey(int usbHidUsage) async {
|
||||
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true);
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false);
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
|
||||
}
|
||||
|
||||
Future<void> onMobileVolumeUp() async =>
|
||||
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage);
|
||||
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF);
|
||||
Future<void> onMobileVolumeDown() async =>
|
||||
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage);
|
||||
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF);
|
||||
Future<void> onMobilePower() async =>
|
||||
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage);
|
||||
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF);
|
||||
}
|
||||
|
||||
@@ -304,8 +304,13 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'job_progress') {
|
||||
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
|
||||
} else if (name == 'job_done') {
|
||||
parent.target?.fileModel.jobController.jobDone(evt);
|
||||
parent.target?.fileModel.refreshAll();
|
||||
bool? refresh =
|
||||
await parent.target?.fileModel.jobController.jobDone(evt);
|
||||
if (refresh == true) {
|
||||
// many job done for delete directory
|
||||
// todo: refresh may not work when confirm delete local directory
|
||||
parent.target?.fileModel.refreshAll();
|
||||
}
|
||||
} else if (name == 'job_error') {
|
||||
parent.target?.fileModel.jobController.jobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
@@ -384,7 +389,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'use_texture_render') {
|
||||
_handleUseTextureRender(evt, sessionId, peerId);
|
||||
} else {
|
||||
debugPrint('Unknown event name: $name');
|
||||
debugPrint('Event is not handled in the fixed branch: $name');
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -492,10 +497,12 @@ class FfiModel with ChangeNotifier {
|
||||
newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
|
||||
newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
|
||||
newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
|
||||
newDisplay.originalWidth =
|
||||
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue;
|
||||
newDisplay.originalHeight =
|
||||
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue;
|
||||
newDisplay.originalWidth = int.tryParse(
|
||||
evt['original_width'] ?? kInvalidResolutionValue.toString()) ??
|
||||
kInvalidResolutionValue;
|
||||
newDisplay.originalHeight = int.tryParse(
|
||||
evt['original_height'] ?? kInvalidResolutionValue.toString()) ??
|
||||
kInvalidResolutionValue;
|
||||
newDisplay._scale = _pi.scaleOfDisplay(display);
|
||||
_pi.displays[display] = newDisplay;
|
||||
|
||||
@@ -723,6 +730,8 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
/// Handle the peer info event based on [evt].
|
||||
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
||||
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
|
||||
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
||||
parent.target?.inputModel.updateKeyboardMode();
|
||||
|
||||
@@ -786,7 +795,7 @@ class FfiModel with ChangeNotifier {
|
||||
isRefreshing = false;
|
||||
}
|
||||
Map<String, dynamic> features = json.decode(evt['features']);
|
||||
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
||||
_pi.features.privacyMode = features['privacy_mode'] == true;
|
||||
if (!isCache) {
|
||||
handleResolutions(peerId, evt["resolutions"]);
|
||||
}
|
||||
@@ -1001,14 +1010,15 @@ class FfiModel with ChangeNotifier {
|
||||
// Notify to switch display
|
||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
||||
'display_is_plugged_out_msg', '', parent.target!.dialogManager);
|
||||
final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex
|
||||
? 0
|
||||
: pi.primaryDisplay;
|
||||
final displays = newDisplay;
|
||||
final isPeerPrimaryDisplayValid =
|
||||
pi.primaryDisplay == kInvalidDisplayIndex ||
|
||||
pi.primaryDisplay >= pi.displays.length;
|
||||
final newDisplay =
|
||||
isPeerPrimaryDisplayValid ? 0 : pi.primaryDisplay;
|
||||
bind.sessionSwitchDisplay(
|
||||
isDesktop: isDesktop,
|
||||
sessionId: sessionId,
|
||||
value: Int32List.fromList([displays]),
|
||||
value: Int32List.fromList([newDisplay]),
|
||||
);
|
||||
|
||||
if (_pi.isSupportMultiUiSession) {
|
||||
@@ -1173,26 +1183,28 @@ class ImageModel with ChangeNotifier {
|
||||
|
||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||
|
||||
onRgba(int display, Uint8List rgba) {
|
||||
clearImage() => _image = null;
|
||||
|
||||
onRgba(int display, Uint8List rgba) async {
|
||||
try {
|
||||
await decodeAndUpdate(display, rgba);
|
||||
} catch (e) {
|
||||
debugPrint('onRgba error: $e');
|
||||
}
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
}
|
||||
|
||||
decodeAndUpdate(int display, Uint8List rgba) async {
|
||||
final pid = parent.target?.id;
|
||||
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
|
||||
img.decodeImageFromPixels(
|
||||
rgba,
|
||||
rect?.width.toInt() ?? 0,
|
||||
rect?.height.toInt() ?? 0,
|
||||
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
||||
onPixelsCopied: () {
|
||||
// Unlock the rgba memory from rust codes.
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
}).then((image) {
|
||||
if (parent.target?.id != pid) return;
|
||||
try {
|
||||
// my throw exception, because the listener maybe already dispose
|
||||
update(image);
|
||||
} catch (e) {
|
||||
debugPrint('update image: $e');
|
||||
}
|
||||
});
|
||||
final image = await img.decodeImageFromPixels(
|
||||
rgba,
|
||||
rect?.width.toInt() ?? 0,
|
||||
rect?.height.toInt() ?? 0,
|
||||
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
|
||||
);
|
||||
if (parent.target?.id != pid) return;
|
||||
await update(image);
|
||||
}
|
||||
|
||||
update(ui.Image? image) async {
|
||||
@@ -2173,6 +2185,7 @@ class CursorModel with ChangeNotifier {
|
||||
debugPrint("deleting cursor with key $k");
|
||||
deleteCustomCursor(k);
|
||||
}
|
||||
resetSystemCursor();
|
||||
}
|
||||
|
||||
trySetRemoteWindowCoords() {
|
||||
@@ -2219,8 +2232,10 @@ class QualityMonitorModel with ChangeNotifier {
|
||||
|
||||
updateQualityStatus(Map<String, dynamic> evt) {
|
||||
try {
|
||||
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed'];
|
||||
if ((evt['fps'] as String).isNotEmpty) {
|
||||
if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) {
|
||||
_data.speed = evt['speed'];
|
||||
}
|
||||
if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) {
|
||||
final fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
|
||||
final pi = parent.target?.ffiModel.pi;
|
||||
if (pi != null) {
|
||||
@@ -2241,14 +2256,18 @@ class QualityMonitorModel with ChangeNotifier {
|
||||
_data.fps = null;
|
||||
}
|
||||
}
|
||||
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay'];
|
||||
if ((evt['target_bitrate'] as String).isNotEmpty) {
|
||||
if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) {
|
||||
_data.delay = evt['delay'];
|
||||
}
|
||||
if (evt.containsKey('target_bitrate') &&
|
||||
(evt['target_bitrate'] as String).isNotEmpty) {
|
||||
_data.targetBitrate = evt['target_bitrate'];
|
||||
}
|
||||
if ((evt['codec_format'] as String).isNotEmpty) {
|
||||
if (evt.containsKey('codec_format') &&
|
||||
(evt['codec_format'] as String).isNotEmpty) {
|
||||
_data.codecFormat = evt['codec_format'];
|
||||
}
|
||||
if ((evt['chroma'] as String).isNotEmpty) {
|
||||
if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) {
|
||||
_data.chroma = evt['chroma'];
|
||||
}
|
||||
notifyListeners();
|
||||
@@ -2492,6 +2511,7 @@ class FFI {
|
||||
onEvent2UIRgba();
|
||||
imageModel.onRgba(display, data);
|
||||
});
|
||||
this.id = id;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2547,32 +2567,30 @@ class FFI {
|
||||
}
|
||||
} else if (message is EventToUI_Rgba) {
|
||||
final display = message.field0;
|
||||
if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) {
|
||||
//debugPrint("EventToUI_Rgba display:$display");
|
||||
textureModel.setTextureType(display: display, gpuTexture: false);
|
||||
// Fetch the image buffer from rust codes.
|
||||
final sz = platformFFI.getRgbaSize(sessionId, display);
|
||||
if (sz == 0) {
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
return;
|
||||
}
|
||||
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
||||
if (rgba != null) {
|
||||
onEvent2UIRgba();
|
||||
await imageModel.onRgba(display, rgba);
|
||||
} else {
|
||||
// Fetch the image buffer from rust codes.
|
||||
final sz = platformFFI.getRgbaSize(sessionId, display);
|
||||
if (sz == 0) {
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
return;
|
||||
}
|
||||
final rgba = platformFFI.getRgba(sessionId, display, sz);
|
||||
if (rgba != null) {
|
||||
onEvent2UIRgba();
|
||||
imageModel.onRgba(display, rgba);
|
||||
} else {
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
}
|
||||
platformFFI.nextRgba(sessionId, display);
|
||||
}
|
||||
} else if (message is EventToUI_Texture) {
|
||||
final display = message.field0;
|
||||
debugPrint("EventToUI_Texture display:$display");
|
||||
if (hasGpuTextureRender) {
|
||||
textureModel.setTextureType(display: display, gpuTexture: true);
|
||||
onEvent2UIRgba();
|
||||
final gpuTexture = message.field1;
|
||||
debugPrint(
|
||||
"EventToUI_Texture display:$display, gpuTexture:$gpuTexture");
|
||||
if (gpuTexture && !hasGpuTextureRender) {
|
||||
debugPrint('the gpuTexture is not supported.');
|
||||
return;
|
||||
}
|
||||
textureModel.setTextureType(display: display, gpuTexture: gpuTexture);
|
||||
onEvent2UIRgba();
|
||||
}
|
||||
}();
|
||||
});
|
||||
@@ -2608,8 +2626,9 @@ class FFI {
|
||||
remember: remember);
|
||||
}
|
||||
|
||||
void send2FA(SessionID sessionId, String code) {
|
||||
bind.sessionSend2Fa(sessionId: sessionId, code: code);
|
||||
void send2FA(SessionID sessionId, String code, bool trustThisDevice) {
|
||||
bind.sessionSend2Fa(
|
||||
sessionId: sessionId, code: code, trustThisDevice: trustThisDevice);
|
||||
}
|
||||
|
||||
/// Close the remote session.
|
||||
@@ -2626,7 +2645,7 @@ class FFI {
|
||||
canvasModel.scale,
|
||||
ffiModel.pi.currentDisplay);
|
||||
}
|
||||
imageModel.update(null);
|
||||
await imageModel.update(null);
|
||||
cursorModel.clear();
|
||||
ffiModel.clear();
|
||||
canvasModel.clear();
|
||||
|
||||
@@ -194,10 +194,14 @@ class Peers extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _updateOnlineState(Map<String, dynamic> evt) {
|
||||
int changedCount = 0;
|
||||
evt['onlines'].split(',').forEach((online) {
|
||||
for (var i = 0; i < peers.length; i++) {
|
||||
if (peers[i].id == online) {
|
||||
peers[i].online = true;
|
||||
if (!peers[i].online) {
|
||||
changedCount += 1;
|
||||
peers[i].online = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -205,13 +209,18 @@ class Peers extends ChangeNotifier {
|
||||
evt['offlines'].split(',').forEach((offline) {
|
||||
for (var i = 0; i < peers.length; i++) {
|
||||
if (peers[i].id == offline) {
|
||||
peers[i].online = false;
|
||||
if (peers[i].online) {
|
||||
changedCount += 1;
|
||||
peers[i].online = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
event = UpdateEvent.online;
|
||||
notifyListeners();
|
||||
if (changedCount > 0) {
|
||||
event = UpdateEvent.online;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _updatePeers(Map<String, dynamic> evt) {
|
||||
|
||||
@@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// `notifyListeners()` will cause many rebuilds.
|
||||
// So, we need to reduce the calls to "notifyListeners()" only when necessary.
|
||||
// A better way is to use a new model.
|
||||
setCurrentTabCachedPeers(List<Peer> peers) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
final isPreEmpty = _currentTabCachedPeers.isEmpty;
|
||||
_currentTabCachedPeers = peers;
|
||||
notifyListeners();
|
||||
final isNowEmpty = _currentTabCachedPeers.isEmpty;
|
||||
if (isPreEmpty != isNowEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ class ServerModel with ChangeNotifier {
|
||||
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
|
||||
} else {
|
||||
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
|
||||
_audioOk = audioOption.isEmpty;
|
||||
_audioOk = audioOption != 'N';
|
||||
}
|
||||
|
||||
// file
|
||||
@@ -206,7 +206,7 @@ class ServerModel with ChangeNotifier {
|
||||
} else {
|
||||
final fileOption =
|
||||
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||
_fileOk = fileOption.isEmpty;
|
||||
_fileOk = fileOption != 'N';
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
@@ -826,7 +826,7 @@ class Client {
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['is_start'] = authorized;
|
||||
data['authorized'] = authorized;
|
||||
data['is_file_transfer'] = isFileTransfer;
|
||||
data['port_forward'] = portForward;
|
||||
data['name'] = name;
|
||||
@@ -840,6 +840,8 @@ class Client {
|
||||
data['block_input'] = blockInput;
|
||||
data['disconnected'] = disconnected;
|
||||
data['from_switch'] = fromSwitch;
|
||||
data['in_voice_call'] = inVoiceCall;
|
||||
data['incoming_voice_call'] = incomingVoiceCall;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,14 @@ class StateGlobal {
|
||||
bool _isMinimized = false;
|
||||
final RxBool isMaximized = false.obs;
|
||||
final RxBool _showTabBar = true.obs;
|
||||
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize);
|
||||
final RxDouble _resizeEdgeSize = RxDouble(windowResizeEdgeSize);
|
||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||
final RxBool showRemoteToolBar = false.obs;
|
||||
final svcStatus = SvcStatus.notReady.obs;
|
||||
final RxBool isFocused = false.obs;
|
||||
|
||||
final isPortrait = false.obs;
|
||||
|
||||
String _inputSource = '';
|
||||
|
||||
// Use for desktop -> remote toolbar -> resolution
|
||||
@@ -93,7 +95,7 @@ class StateGlobal {
|
||||
? kFullScreenEdgeSize
|
||||
: isMaximized.isTrue
|
||||
? kMaximizeEdgeSize
|
||||
: windowEdgeSize;
|
||||
: windowResizeEdgeSize;
|
||||
|
||||
String getInputSource({bool force = false}) {
|
||||
if (force || _inputSource.isEmpty) {
|
||||
|
||||
@@ -17,13 +17,23 @@ bool refreshingUser = false;
|
||||
class UserModel {
|
||||
final RxString userName = ''.obs;
|
||||
final RxBool isAdmin = false.obs;
|
||||
final RxString networkError = ''.obs;
|
||||
bool get isLogin => userName.isNotEmpty;
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
UserModel(this.parent);
|
||||
UserModel(this.parent) {
|
||||
userName.listen((p0) {
|
||||
// When user name becomes empty, show login button
|
||||
// When user name becomes non-empty:
|
||||
// For _updateLocalUserInfo, network error will be set later
|
||||
// For login success, should clear network error
|
||||
networkError.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
void refreshCurrentUser() async {
|
||||
if (bind.isDisableAccount()) return;
|
||||
networkError.value = '';
|
||||
final token = bind.mainGetLocalOption(key: 'access_token');
|
||||
if (token == '') {
|
||||
await updateOtherModels();
|
||||
@@ -38,12 +48,18 @@ class UserModel {
|
||||
if (refreshingUser) return;
|
||||
try {
|
||||
refreshingUser = true;
|
||||
final response = await http.post(Uri.parse('$url/api/currentUser'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token'
|
||||
},
|
||||
body: json.encode(body));
|
||||
final http.Response response;
|
||||
try {
|
||||
response = await http.post(Uri.parse('$url/api/currentUser'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token'
|
||||
},
|
||||
body: json.encode(body));
|
||||
} catch (e) {
|
||||
networkError.value = e.toString();
|
||||
rethrow;
|
||||
}
|
||||
refreshingUser = false;
|
||||
final status = response.statusCode;
|
||||
if (status == 401 || status == 400) {
|
||||
|
||||
@@ -11,3 +11,7 @@ final isWebDesktop_ = false;
|
||||
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
|
||||
String get screenInfo_ => '';
|
||||
|
||||
final isWebOnWindows_ = false;
|
||||
final isWebOnLinux_ = false;
|
||||
final isWebOnMacOS_ = false;
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/model.dart';
|
||||
|
||||
deleteCustomCursor(String key) =>
|
||||
custom_cursor_manager.CursorManager.instance.deleteCursor(key);
|
||||
resetSystemCursor() {}
|
||||
|
||||
MouseCursor buildCursorOfCache(
|
||||
CursorModel cursor, double scale, CursorData? cache) {
|
||||
|
||||
@@ -13,14 +13,12 @@ Future<ui.Image?> decodeImageFromPixels(
|
||||
int? rowBytes,
|
||||
int? targetWidth,
|
||||
int? targetHeight,
|
||||
VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds
|
||||
bool allowUpscaling = true,
|
||||
}) async {
|
||||
if (targetWidth != null) {
|
||||
assert(allowUpscaling || targetWidth <= width);
|
||||
if (!(allowUpscaling || targetWidth <= width)) {
|
||||
print("not allow upscaling but targetWidth > width");
|
||||
onPixelsCopied?.call();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +26,6 @@ Future<ui.Image?> decodeImageFromPixels(
|
||||
assert(allowUpscaling || targetHeight <= height);
|
||||
if (!(allowUpscaling || targetHeight <= height)) {
|
||||
print("not allow upscaling but targetHeight > height");
|
||||
onPixelsCopied?.call();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -36,9 +33,7 @@ Future<ui.Image?> decodeImageFromPixels(
|
||||
final ui.ImmutableBuffer buffer;
|
||||
try {
|
||||
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||
onPixelsCopied?.call();
|
||||
} catch (e) {
|
||||
onPixelsCopied?.call();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'dart:html' as html;
|
||||
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
|
||||
@@ -23,6 +24,7 @@ sealed class EventToUI {
|
||||
) = EventToUI_Rgba;
|
||||
const factory EventToUI.texture(
|
||||
int field0,
|
||||
bool field1,
|
||||
) = EventToUI_Texture;
|
||||
}
|
||||
|
||||
@@ -33,15 +35,19 @@ class EventToUI_Event implements EventToUI {
|
||||
}
|
||||
|
||||
class EventToUI_Rgba implements EventToUI {
|
||||
const EventToUI_Rgba(final int field0) : this.field = field0;
|
||||
const EventToUI_Rgba(final int field0) : field = field0;
|
||||
final int field;
|
||||
int get field0 => field;
|
||||
}
|
||||
|
||||
class EventToUI_Texture implements EventToUI {
|
||||
const EventToUI_Texture(final int field0) : this.field = field0;
|
||||
final int field;
|
||||
int get field0 => field;
|
||||
const EventToUI_Texture(final int field0, final bool field1)
|
||||
: f0 = field0,
|
||||
f1 = field1;
|
||||
final int f0;
|
||||
final bool f1;
|
||||
int get field0 => f0;
|
||||
bool get field1 => f1;
|
||||
}
|
||||
|
||||
class RustdeskImpl {
|
||||
@@ -142,7 +148,10 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<void> sessionSend2Fa(
|
||||
{required UuidValue sessionId, required String code, dynamic hint}) {
|
||||
{required UuidValue sessionId,
|
||||
required String code,
|
||||
required bool trustThisDevice,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', ['send_2fa', code]));
|
||||
}
|
||||
|
||||
@@ -178,7 +187,7 @@ class RustdeskImpl {
|
||||
Future<void> sessionToggleOption(
|
||||
{required UuidValue sessionId, required String value, dynamic hint}) {
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ['toggle_option', value]));
|
||||
() => js.context.callMethod('setByName', ['option:toggle', value]));
|
||||
}
|
||||
|
||||
Future<void> sessionTogglePrivacyMode(
|
||||
@@ -187,8 +196,8 @@ class RustdeskImpl {
|
||||
required bool on,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'toggle_option',
|
||||
jsonEncode({implKey, on})
|
||||
'toggle_privacy_mode',
|
||||
jsonEncode({'impl_key': implKey, 'on': on})
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -226,7 +235,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String getLocalKbLayoutType({dynamic hint}) {
|
||||
throw js.context.callMethod('getByName', ['option:local', 'kb_layout']);
|
||||
return js.context.callMethod('getByName', ['option:local', 'kb_layout']);
|
||||
}
|
||||
|
||||
Future<void> setLocalKbLayoutType(
|
||||
@@ -343,7 +352,7 @@ class RustdeskImpl {
|
||||
|
||||
bool sessionIsKeyboardModeSupported(
|
||||
{required UuidValue sessionId, required String mode, dynamic hint}) {
|
||||
return mode == kKeyLegacyMode;
|
||||
return [kKeyLegacyMode, kKeyMapMode].contains(mode);
|
||||
}
|
||||
|
||||
bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
|
||||
@@ -382,14 +391,32 @@ class RustdeskImpl {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'switch_display',
|
||||
jsonEncode({
|
||||
isDesktop: isDesktop,
|
||||
sessionId: sessionId.toString(),
|
||||
value: value
|
||||
'isDesktop': isDesktop,
|
||||
'sessionId': sessionId.toString(),
|
||||
'value': value
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionHandleFlutterKeyEvent(
|
||||
{required UuidValue sessionId,
|
||||
required String character,
|
||||
required int usbHid,
|
||||
required int lockModes,
|
||||
required bool downOrUp,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'flutter_key_event',
|
||||
jsonEncode({
|
||||
'name': character,
|
||||
'usb_hid': usbHid,
|
||||
'lock_modes': lockModes,
|
||||
if (downOrUp) 'down': 'true',
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionHandleFlutterRawKeyEvent(
|
||||
{required UuidValue sessionId,
|
||||
required String name,
|
||||
required int platformCode,
|
||||
@@ -397,7 +424,6 @@ class RustdeskImpl {
|
||||
required int lockModes,
|
||||
required bool downOrUp,
|
||||
dynamic hint}) {
|
||||
// TODO: map mode
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -630,7 +656,15 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainGetLoginDeviceInfo({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
String userAgent = html.window.navigator.userAgent;
|
||||
String appName = html.window.navigator.appName;
|
||||
String appVersion = html.window.navigator.appVersion;
|
||||
String? platform = html.window.navigator.platform;
|
||||
return jsonEncode({
|
||||
'os': '$userAgent, $appName $appVersion ($platform)',
|
||||
'type': 'Web client',
|
||||
'name': js.context.callMethod('getByName', ['my_name']),
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> mainChangeId({required String newId, dynamic hint}) {
|
||||
@@ -699,11 +733,11 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetAppName({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future.value(mainGetAppNameSync(hint: hint));
|
||||
}
|
||||
|
||||
String mainGetAppNameSync({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return 'RustDesk';
|
||||
}
|
||||
|
||||
String mainUriPrefixSync({dynamic hint}) {
|
||||
@@ -711,7 +745,8 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetLicense({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
// TODO: implement
|
||||
return Future(() => '');
|
||||
}
|
||||
|
||||
Future<String> mainGetVersion({dynamic hint}) {
|
||||
@@ -755,8 +790,9 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<bool> mainIsUsingPublicServer({dynamic hint}) {
|
||||
return Future(
|
||||
() => js.context.callMethod('setByName', ["is_using_public_server"]));
|
||||
return Future(() =>
|
||||
js.context.callMethod('getByName', ["is_using_public_server"]) ==
|
||||
'true');
|
||||
}
|
||||
|
||||
Future<void> mainDiscover({dynamic hint}) {
|
||||
@@ -823,11 +859,11 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetMyId({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('getByName', ['my_id']));
|
||||
}
|
||||
|
||||
Future<String> mainGetUuid({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('getByName', ['uuid']));
|
||||
}
|
||||
|
||||
Future<String> mainGetPeerOption(
|
||||
@@ -949,10 +985,11 @@ class RustdeskImpl {
|
||||
|
||||
Future<void> mainSetUserDefaultOption(
|
||||
{required String key, required String value, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', [
|
||||
js.context.callMethod('setByName', [
|
||||
'option:user:default',
|
||||
jsonEncode({'name': key, 'value': value})
|
||||
]);
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
String mainGetUserDefaultOption({required String key, dynamic hint}) {
|
||||
@@ -1026,7 +1063,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetLangs({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('getByName', ['langs']));
|
||||
}
|
||||
|
||||
Future<String> mainGetTemporaryPassword({dynamic hint}) {
|
||||
@@ -1038,7 +1075,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetFingerprint({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return Future.value('');
|
||||
}
|
||||
|
||||
Future<String> cmGetClientsState({dynamic hint}) {
|
||||
@@ -1080,7 +1117,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
String mainSupportedHwdecodings({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return '{}';
|
||||
}
|
||||
|
||||
Future<bool> mainIsRoot({dynamic hint}) {
|
||||
@@ -1167,8 +1204,10 @@ class RustdeskImpl {
|
||||
required int index,
|
||||
required bool on,
|
||||
dynamic hint}) {
|
||||
// TODO
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'toggle_virtual_display',
|
||||
jsonEncode({'index': index, 'on': on})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> mainSetHomeDir({required String home, dynamic hint}) {
|
||||
@@ -1269,8 +1308,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
Future<String> mainGetBuildDate({dynamic hint}) {
|
||||
// TODO
|
||||
throw UnimplementedError();
|
||||
return Future(() => js.context.callMethod('getByName', ['build_date']));
|
||||
}
|
||||
|
||||
String translate(
|
||||
@@ -1412,7 +1450,7 @@ class RustdeskImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mainHideDocker({dynamic hint}) {
|
||||
bool mainHideDock({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -1607,7 +1645,7 @@ class RustdeskImpl {
|
||||
}
|
||||
|
||||
bool mainIsOptionFixed({required String key, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mainGetUseTextureRender({dynamic hint}) {
|
||||
@@ -1622,5 +1660,65 @@ class RustdeskImpl {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
String mainGetUnlockPin({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
String mainSetUnlockPin({required String pin, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool sessionGetEnableTrustedDevices(
|
||||
{required UuidValue sessionId, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<String> mainGetTrustedDevices({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> mainRemoveTrustedDevices({required String json, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> mainClearTrustedDevices({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<String> getVoiceCallInputDevice({required bool isCm, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<void> setVoiceCallInputDevice(
|
||||
{required bool isCm, required String device, dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool isPresetPasswordMobileOnly({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
String mainGetBuildinOption({required String key, dynamic hint}) {
|
||||
return '';
|
||||
}
|
||||
|
||||
String installInstallOptions({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
int mainMaxEncryptLen({dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
sessionRenameFile(
|
||||
{required UuidValue sessionId,
|
||||
required int actId,
|
||||
required String path,
|
||||
required String newName,
|
||||
required bool isRemote,
|
||||
dynamic hint}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:js' as js;
|
||||
import 'dart:html' as html;
|
||||
|
||||
final isAndroid_ = false;
|
||||
final isIOS_ = false;
|
||||
@@ -11,3 +12,9 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
|
||||
final isDesktop_ = false;
|
||||
|
||||
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
|
||||
|
||||
final _userAgent = html.window.navigator.userAgent.toLowerCase();
|
||||
|
||||
final isWebOnWindows_ = _userAgent.contains('win');
|
||||
final isWebOnLinux_ = _userAgent.contains('linux');
|
||||
final isWebOnMacOS_ = _userAgent.contains('mac');
|
||||
|
||||
@@ -58,6 +58,11 @@ class CursorManager {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetSystemCursor() async {
|
||||
latestKey = '';
|
||||
js.context.callMethod('setByName', ['cursor', 'auto']);
|
||||
}
|
||||
}
|
||||
|
||||
class FlutterCustomMemoryImageCursor extends MouseCursor {
|
||||
@@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession {
|
||||
}
|
||||
|
||||
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
|
||||
resetSystemCursor() => CursorManager.instance.resetSystemCursor();
|
||||
|
||||
MouseCursor buildCursorOfCache(
|
||||
model.CursorModel cursor, double scale, model.CursorData? cache) {
|
||||
|
||||
98
flutter/lib/web/settings_page.dart
Normal file
98
flutter/lib/web/settings_page.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/scan_page.dart';
|
||||
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../models/model.dart';
|
||||
|
||||
class WebSettingsPage extends StatelessWidget {
|
||||
const WebSettingsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isWebDesktop) {
|
||||
return _buildDesktopButton(context);
|
||||
} else {
|
||||
return _buildMobileMenu(context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDesktopButton(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
DesktopSettingPage(initialTabkey: SettingsTabKey.general),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileMenu(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
return PopupMenuButton<String>(
|
||||
tooltip: "",
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
return (isIOS
|
||||
? [
|
||||
const PopupMenuItem(
|
||||
value: "scan",
|
||||
child: Icon(Icons.qr_code_scanner, color: Colors.black),
|
||||
)
|
||||
]
|
||||
: <PopupMenuItem<String>>[]) +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "server",
|
||||
child: Text(translate('ID/Relay Server')),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "login",
|
||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate("Login")
|
||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "about",
|
||||
child: Text(translate('About RustDesk')),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 'server') {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'about') {
|
||||
showAbout(gFFI.dialogManager);
|
||||
}
|
||||
if (value == 'login') {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
} else {
|
||||
logOutConfirmDialog();
|
||||
}
|
||||
}
|
||||
if (value == 'scan') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ScanPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class TextureRgbaRenderer {
|
||||
}
|
||||
|
||||
Future<bool> closeTexture(int key) {
|
||||
throw UnimplementedError();
|
||||
return Future(() => true);
|
||||
}
|
||||
|
||||
Future<bool> onRgba(
|
||||
|
||||
@@ -95,17 +95,17 @@ SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
|
||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
|
||||
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||
|
||||
@@ -45,5 +45,7 @@
|
||||
<string>Record the sound from microphone for the purpose of the remote desktop.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>LSUIElement</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
2
flutter/ndk_x86.sh
Executable file
2
flutter/ndk_x86.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter
|
||||
@@ -335,7 +335,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "336308d86ec8b9640504a371b50ba500eb779363"
|
||||
resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926"
|
||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
@@ -377,9 +377,25 @@ packages:
|
||||
path: "."
|
||||
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
|
||||
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
|
||||
url: "https://github.com/21pages/dynamic_layouts.git"
|
||||
url: "https://github.com/rustdesk-org/dynamic_layouts.git"
|
||||
source: git
|
||||
version: "0.0.1+1"
|
||||
extended_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_text
|
||||
sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
extended_text_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: extended_text_library
|
||||
sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.0"
|
||||
external_path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -509,9 +525,9 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
||||
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3"
|
||||
url: "https://github.com/21pages/flutter_gpu_texture_renderer"
|
||||
ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
|
||||
resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
|
||||
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
flutter_improved_scrolling:
|
||||
@@ -1613,5 +1629,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
sdks:
|
||||
dart: ">=3.2.0 <4.0.0"
|
||||
flutter: ">=3.16.0"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.2.7+46
|
||||
version: 1.3.1+47
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
@@ -92,18 +92,19 @@ dependencies:
|
||||
dropdown_button2: ^2.0.0
|
||||
flutter_gpu_texture_renderer:
|
||||
git:
|
||||
url: https://github.com/21pages/flutter_gpu_texture_renderer
|
||||
ref: 38951317afe79d953ab25733667bd96e172a80d3
|
||||
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
|
||||
ref: 2ded7f146437a761ffe6981e2f742038f85ca68d
|
||||
uuid: ^3.0.7
|
||||
auto_size_text_field: ^2.2.1
|
||||
flex_color_picker: ^3.3.0
|
||||
dynamic_layouts:
|
||||
git:
|
||||
url: https://github.com/21pages/dynamic_layouts.git
|
||||
url: https://github.com/rustdesk-org/dynamic_layouts.git
|
||||
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
|
||||
pull_down_button: ^0.9.3
|
||||
device_info_plus: ^9.1.0
|
||||
qr_flutter: ^4.1.0
|
||||
extended_text: 13.0.0
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 460 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 481 KiB |
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
use hbb_common::{allow_err, log};
|
||||
use hbb_common::{allow_err, bail};
|
||||
use hbb_common::{
|
||||
lazy_static,
|
||||
tokio::sync::{
|
||||
@@ -25,6 +25,8 @@ pub use context_send::*;
|
||||
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
|
||||
|
||||
pub(crate) use platform::create_cliprdr_context;
|
||||
|
||||
@@ -130,7 +132,7 @@ impl ClipboardFile {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_stopping_allowed_from_peer(&self) -> bool {
|
||||
pub fn is_beginning_message(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
|
||||
@@ -198,7 +200,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
#[inline]
|
||||
fn send_data(conn_id: i32, data: ClipboardFile) {
|
||||
fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return send_data_to_channel(conn_id, data);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -210,25 +212,28 @@ fn send_data(conn_id: i32, data: ClipboardFile) {
|
||||
}
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
#[inline]
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) {
|
||||
// no need to handle result here
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||
if let Some(msg_channel) = VEC_MSG_CHANNEL
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|x| x.conn_id == conn_id)
|
||||
{
|
||||
allow_err!(msg_channel.sender.send(data));
|
||||
msg_channel.sender.send(data)?;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("conn_id not found");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[inline]
|
||||
fn send_data_to_all(data: ClipboardFile) {
|
||||
// no need to handle result here
|
||||
fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
|
||||
// Need more tests to see if it's necessary to handle the error.
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use crate::{CliprdrError, CliprdrServiceContext};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -63,8 +64,10 @@ pub fn create_cliprdr_context(
|
||||
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
struct DummyCliprdrContext {}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl CliprdrServiceContext for DummyCliprdrContext {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
|
||||
Ok(())
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use crate::{
|
||||
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 std::{
|
||||
@@ -998,7 +998,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE)
|
||||
}
|
||||
};
|
||||
// no need to handle result here
|
||||
send_data(conn_id as _, data);
|
||||
allow_err!(send_data(conn_id as _, data));
|
||||
|
||||
0
|
||||
}
|
||||
@@ -1045,7 +1045,13 @@ extern "C" fn client_format_list(
|
||||
.iter()
|
||||
.for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone())));
|
||||
} else {
|
||||
send_data(conn_id, data);
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("failed to send format list: {:?}", e);
|
||||
return ERR_CODE_SEND_MSG;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
|
||||
@@ -1067,9 +1073,13 @@ extern "C" fn client_format_list_response(
|
||||
msg_flags
|
||||
);
|
||||
let data = ClipboardFile::FormatListResponse { msg_flags };
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send format list response: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn client_format_data_request(
|
||||
@@ -1090,10 +1100,13 @@ extern "C" fn client_format_data_request(
|
||||
conn_id,
|
||||
requested_format_id
|
||||
);
|
||||
// no need to handle result here
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send format data request: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn client_format_data_response(
|
||||
@@ -1125,9 +1138,13 @@ extern "C" fn client_format_data_response(
|
||||
msg_flags,
|
||||
format_data,
|
||||
};
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send format data response: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn client_file_contents_request(
|
||||
@@ -1175,9 +1192,13 @@ extern "C" fn client_file_contents_request(
|
||||
clip_data_id,
|
||||
};
|
||||
log::debug!("client_file_contents_request called, data: {:?}", &data);
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send file contents request: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn client_file_contents_response(
|
||||
@@ -1213,7 +1234,11 @@ extern "C" fn client_file_contents_response(
|
||||
msg_flags,
|
||||
stream_id
|
||||
);
|
||||
send_data(conn_id, data);
|
||||
|
||||
0
|
||||
match send_data(conn_id, data) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to send file contents response: {:?}", e);
|
||||
ERR_CODE_SEND_MSG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,8 @@ struct wf_clipboard
|
||||
HWND hwnd;
|
||||
HANDLE hmem;
|
||||
HANDLE thread;
|
||||
HANDLE response_data_event;
|
||||
HANDLE formatDataRespEvent;
|
||||
BOOL formatDataRespReceived;
|
||||
|
||||
LPDATAOBJECT data_obj;
|
||||
HANDLE data_obj_mutex;
|
||||
@@ -228,6 +229,7 @@ struct wf_clipboard
|
||||
ULONG req_fsize;
|
||||
char *req_fdata;
|
||||
HANDLE req_fevent;
|
||||
BOOL req_f_received;
|
||||
|
||||
size_t nFiles;
|
||||
size_t file_array_size;
|
||||
@@ -287,6 +289,9 @@ static BOOL try_open_clipboard(HWND hwnd)
|
||||
static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid,
|
||||
void **ppvObject)
|
||||
{
|
||||
if (ppvObject == NULL)
|
||||
return E_INVALIDARG;
|
||||
|
||||
if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown))
|
||||
{
|
||||
IStream_AddRef(This);
|
||||
@@ -362,6 +367,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
|
||||
}
|
||||
|
||||
*pcbRead = clipboard->req_fsize;
|
||||
// Check overflow, can not be a real case
|
||||
if ((instance->m_lOffset.QuadPart + clipboard->req_fsize) < instance->m_lOffset.QuadPart) {
|
||||
// It's better to crash to release the explorer.exe
|
||||
// This is a critical error, because the explorer is waiting for the data
|
||||
// and the m_lOffset is wrong(overflowed)
|
||||
return S_FALSE;
|
||||
}
|
||||
instance->m_lOffset.QuadPart += clipboard->req_fsize;
|
||||
|
||||
if (clipboard->req_fsize < cb)
|
||||
@@ -517,11 +529,17 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **pp
|
||||
|
||||
static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc)
|
||||
{
|
||||
IStream *iStream;
|
||||
IStream *iStream = NULL;
|
||||
BOOL success = FALSE;
|
||||
BOOL isDir = FALSE;
|
||||
CliprdrStream *instance;
|
||||
CliprdrStream *instance = NULL;
|
||||
wfClipboard *clipboard = (wfClipboard *)pData;
|
||||
|
||||
if (!(pData && dsc))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream));
|
||||
|
||||
if (instance)
|
||||
@@ -874,14 +892,18 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This
|
||||
static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count,
|
||||
void *data)
|
||||
{
|
||||
CliprdrDataObject *instance;
|
||||
IDataObject *iDataObject;
|
||||
CliprdrDataObject *instance = NULL;
|
||||
IDataObject *iDataObject = NULL;
|
||||
instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject));
|
||||
|
||||
if (!instance)
|
||||
goto error;
|
||||
|
||||
instance->m_pFormatEtc = NULL;
|
||||
instance->m_pStgMedium = NULL;
|
||||
|
||||
iDataObject = &instance->iDataObject;
|
||||
iDataObject->lpVtbl = NULL;
|
||||
iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl));
|
||||
|
||||
if (!iDataObject->lpVtbl)
|
||||
@@ -929,7 +951,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc
|
||||
|
||||
return instance;
|
||||
error:
|
||||
CliprdrDataObject_Delete(instance);
|
||||
if (iDataObject && iDataObject->lpVtbl)
|
||||
{
|
||||
free(iDataObject->lpVtbl);
|
||||
}
|
||||
if (instance)
|
||||
{
|
||||
if (instance->m_pFormatEtc)
|
||||
{
|
||||
free(instance->m_pFormatEtc);
|
||||
}
|
||||
|
||||
if (instance->m_pStgMedium)
|
||||
{
|
||||
free(instance->m_pStgMedium);
|
||||
}
|
||||
|
||||
CliprdrDataObject_Delete(instance);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1010,6 +1049,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT
|
||||
REFIID riid, void **ppvObject)
|
||||
{
|
||||
(void)This;
|
||||
if (!ppvObject)
|
||||
return E_INVALIDARG;
|
||||
|
||||
if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown))
|
||||
{
|
||||
@@ -1198,6 +1239,7 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
|
||||
WCHAR *unicode_name;
|
||||
#if !defined(UNICODE)
|
||||
size_t size;
|
||||
int towchar_count;
|
||||
#endif
|
||||
|
||||
if (!clipboard || !format_name)
|
||||
@@ -1205,6 +1247,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
|
||||
|
||||
#if defined(UNICODE)
|
||||
unicode_name = _wcsdup(format_name);
|
||||
if (!unicode_name)
|
||||
return 0;
|
||||
#else
|
||||
size = _tcslen(format_name);
|
||||
unicode_name = calloc(size + 1, sizeof(WCHAR));
|
||||
@@ -1212,11 +1256,13 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
|
||||
if (!unicode_name)
|
||||
return 0;
|
||||
|
||||
MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size);
|
||||
#endif
|
||||
|
||||
if (!unicode_name)
|
||||
towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0);
|
||||
if (towchar_count <= 0 || towchar_count > size)
|
||||
return 0;
|
||||
towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size);
|
||||
if (towchar_count <= 0)
|
||||
return 0;
|
||||
#endif
|
||||
|
||||
for (i = 0; i < clipboard->map_size; i++)
|
||||
{
|
||||
@@ -1312,6 +1358,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
||||
if (!clipboard)
|
||||
return -1;
|
||||
|
||||
// to-do:
|
||||
// Directly use the environment variable `TEMP` is not safe.
|
||||
// But this function is not used for now.
|
||||
if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) ==
|
||||
0)
|
||||
return -1;
|
||||
@@ -1444,7 +1493,37 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID)
|
||||
return rc;
|
||||
}
|
||||
|
||||
UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, void **data)
|
||||
// Ensure the event is not signaled, and reset it if it is.
|
||||
UINT try_reset_event(HANDLE event)
|
||||
{
|
||||
if (!event)
|
||||
{
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
DWORD result = WaitForSingleObject(event, 0);
|
||||
if (result == WAIT_OBJECT_0)
|
||||
{
|
||||
if (!ResetEvent(event))
|
||||
{
|
||||
return GetLastError();
|
||||
}
|
||||
else
|
||||
{
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
else if (result == WAIT_TIMEOUT)
|
||||
{
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data)
|
||||
{
|
||||
UINT rc = ERROR_SUCCESS;
|
||||
clipboard->context->IsStopped = FALSE;
|
||||
@@ -1456,7 +1535,21 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
|
||||
DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis);
|
||||
if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE)
|
||||
{
|
||||
continue;
|
||||
if ((*recvedFlag) == TRUE) {
|
||||
// The data has been received, but the event is still not signaled.
|
||||
// We just skip the rest of the waiting and reset the flag.
|
||||
*recvedFlag = FALSE;
|
||||
// Explicitly set the waitRes to WAIT_OBJECT_0, because we have received the data.
|
||||
waitRes = WAIT_OBJECT_0;
|
||||
} else {
|
||||
// The data has not been received yet, we should continue to wait.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ResetEvent(event))
|
||||
{
|
||||
// NOTE: critical error here, crash may be better
|
||||
}
|
||||
|
||||
if (clipboard->context->IsStopped == TRUE)
|
||||
@@ -1470,12 +1563,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
if (!ResetEvent(event))
|
||||
{
|
||||
// NOTE: critical error here, crash may be better
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
if ((*data) == NULL)
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
@@ -1519,6 +1606,13 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
rc = try_reset_event(clipboard->formatDataRespEvent);
|
||||
if (rc != ERROR_SUCCESS)
|
||||
{
|
||||
return rc;
|
||||
}
|
||||
clipboard->formatDataRespReceived = FALSE;
|
||||
|
||||
remoteFormatId = get_remote_format_id(clipboard, formatId);
|
||||
|
||||
formatDataRequest.connID = connID;
|
||||
@@ -1530,7 +1624,7 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
||||
return rc;
|
||||
}
|
||||
|
||||
wait_response_event(connID, clipboard, clipboard->response_data_event, &clipboard->hmem);
|
||||
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
||||
}
|
||||
|
||||
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
|
||||
@@ -1543,7 +1637,17 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
rc = try_reset_event(clipboard->req_fevent);
|
||||
if (rc != ERROR_SUCCESS)
|
||||
{
|
||||
return rc;
|
||||
}
|
||||
clipboard->req_f_received = FALSE;
|
||||
|
||||
fileContentsRequest.connID = connID;
|
||||
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
|
||||
// But it is OK, because it is only used to check if the stream is the same in
|
||||
// `wf_cliprdr_server_file_contents_request()` function.
|
||||
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
|
||||
fileContentsRequest.listIndex = index;
|
||||
fileContentsRequest.dwFlags = flag;
|
||||
@@ -1558,7 +1662,7 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
||||
return rc;
|
||||
}
|
||||
|
||||
return wait_response_event(connID, clipboard, clipboard->req_fevent, (void **)&clipboard->req_fdata);
|
||||
return wait_response_event(connID, clipboard, clipboard->req_fevent, &clipboard->req_f_received, (void **)&clipboard->req_fdata);
|
||||
}
|
||||
|
||||
static UINT cliprdr_send_response_filecontents(
|
||||
@@ -1788,6 +1892,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM
|
||||
break;
|
||||
|
||||
case WM_DESTROYCLIPBOARD:
|
||||
// to-do: clear clipboard data?
|
||||
case WM_ASKCBFORMATNAME:
|
||||
case WM_HSCROLLCLIPBOARD:
|
||||
case WM_PAINTCLIPBOARD:
|
||||
@@ -1904,7 +2009,7 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po
|
||||
LONG positionHigh, DWORD nRequested, DWORD *puSize)
|
||||
{
|
||||
BOOL res = FALSE;
|
||||
HANDLE hFile;
|
||||
HANDLE hFile = NULL;
|
||||
DWORD nGet, rc;
|
||||
|
||||
if (!file_name || !buffer || !puSize)
|
||||
@@ -1932,9 +2037,11 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po
|
||||
|
||||
res = TRUE;
|
||||
error:
|
||||
|
||||
if (!CloseHandle(hFile))
|
||||
res = FALSE;
|
||||
if (hFile)
|
||||
{
|
||||
if (!CloseHandle(hFile))
|
||||
res = FALSE;
|
||||
}
|
||||
|
||||
if (res)
|
||||
*puSize = nGet;
|
||||
@@ -1945,8 +2052,8 @@ error:
|
||||
/* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */
|
||||
static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen)
|
||||
{
|
||||
HANDLE hFile;
|
||||
FILEDESCRIPTORW *fd;
|
||||
HANDLE hFile = NULL;
|
||||
FILEDESCRIPTORW *fd = NULL;
|
||||
fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW));
|
||||
|
||||
if (!fd)
|
||||
@@ -1975,7 +2082,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
|
||||
}
|
||||
|
||||
fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh);
|
||||
wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen);
|
||||
if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0]))
|
||||
{
|
||||
// The file name is too long, which is not a normal case.
|
||||
// So we just return NULL.
|
||||
CloseHandle(hFile);
|
||||
free(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
wcsncpy_s(fd->cFileName, sizeof(fd->cFileName) / sizeof(fd->cFileName[0]), file_name + pathLen, wcslen(file_name + pathLen) + 1);
|
||||
CloseHandle(hFile);
|
||||
|
||||
return fd;
|
||||
@@ -2024,7 +2140,12 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
|
||||
if (!clipboard->file_names[clipboard->nFiles])
|
||||
return FALSE;
|
||||
|
||||
wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name);
|
||||
// `MAX_PATH` is long enough for the file name.
|
||||
// So we just return FALSE if the file name is too long, which is not a normal case.
|
||||
if ((wcslen(full_file_name) + 1) > MAX_PATH)
|
||||
return FALSE;
|
||||
|
||||
wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1);
|
||||
/* add to descriptor array */
|
||||
clipboard->fileDescriptor[clipboard->nFiles] =
|
||||
wf_cliprdr_get_file_descriptor(full_file_name, pathLen);
|
||||
@@ -2048,8 +2169,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
|
||||
if (!clipboard || !Dir)
|
||||
return FALSE;
|
||||
|
||||
// StringCchCopy(DirSpec, MAX_PATH, Dir);
|
||||
// StringCchCat(DirSpec, MAX_PATH, TEXT("\\*"));
|
||||
if (wcslen(Dir) + 3 > MAX_PATH)
|
||||
return FALSE;
|
||||
StringCchCopyW(DirSpec, MAX_PATH, Dir);
|
||||
StringCchCatW(DirSpec, MAX_PATH, L"\\*");
|
||||
|
||||
@@ -2078,9 +2199,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
|
||||
if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
||||
{
|
||||
WCHAR DirAdd[MAX_PATH];
|
||||
// StringCchCopy(DirAdd, MAX_PATH, Dir);
|
||||
// StringCchCat(DirAdd, MAX_PATH, _T("\\"));
|
||||
// StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName);
|
||||
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
|
||||
return FALSE;
|
||||
StringCchCopyW(DirAdd, MAX_PATH, Dir);
|
||||
StringCchCatW(DirAdd, MAX_PATH, L"\\");
|
||||
StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName);
|
||||
@@ -2094,10 +2214,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
|
||||
else
|
||||
{
|
||||
WCHAR fileName[MAX_PATH];
|
||||
// StringCchCopy(fileName, MAX_PATH, Dir);
|
||||
// StringCchCat(fileName, MAX_PATH, _T("\\"));
|
||||
// StringCchCat(fileName, MAX_PATH, FindFileData.cFileName);
|
||||
|
||||
if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
|
||||
return FALSE;
|
||||
StringCchCopyW(fileName, MAX_PATH, Dir);
|
||||
StringCchCatW(fileName, MAX_PATH, L"\\");
|
||||
StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName);
|
||||
@@ -2242,9 +2360,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
if (context->EnableFiles)
|
||||
{
|
||||
UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32));
|
||||
*p_conn_id = formatList->connID;
|
||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
|
||||
rc = CHANNEL_RC_OK;
|
||||
if (p_conn_id) {
|
||||
*p_conn_id = formatList->connID;
|
||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
|
||||
rc = CHANNEL_RC_OK;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2265,16 +2385,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
||||
// SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL);
|
||||
|
||||
FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS));
|
||||
format_ids->connID = formatList->connID;
|
||||
format_ids->size = (UINT32)clipboard->map_size;
|
||||
format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
|
||||
for (i = 0; i < format_ids->size; ++i)
|
||||
if (format_ids)
|
||||
{
|
||||
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id;
|
||||
}
|
||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids))
|
||||
{
|
||||
rc = CHANNEL_RC_OK;
|
||||
format_ids->connID = formatList->connID;
|
||||
format_ids->size = (UINT32)clipboard->map_size;
|
||||
format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
|
||||
if (format_ids->formats)
|
||||
{
|
||||
for (i = 0; i < format_ids->size; ++i)
|
||||
{
|
||||
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id;
|
||||
}
|
||||
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids))
|
||||
{
|
||||
rc = CHANNEL_RC_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2469,17 +2603,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
|
||||
p += len + 1, clipboard->nFiles++)
|
||||
{
|
||||
int cchWideChar;
|
||||
WCHAR *wFileName;
|
||||
cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0);
|
||||
wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR));
|
||||
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar);
|
||||
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar);
|
||||
if (wFileName)
|
||||
{
|
||||
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar);
|
||||
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar);
|
||||
free(wFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
GlobalUnlock(stg_medium.hGlobal);
|
||||
ReleaseStgMedium(&stg_medium);
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalUnlock(stg_medium.hGlobal);
|
||||
ReleaseStgMedium(&stg_medium);
|
||||
resp:
|
||||
// size will not overflow, because size type is size_t (unsigned __int64)
|
||||
size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW);
|
||||
groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size);
|
||||
|
||||
@@ -2519,10 +2664,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
|
||||
globlemem = (char *)GlobalLock(hClipdata);
|
||||
size = (int)GlobalSize(hClipdata);
|
||||
buff = malloc(size);
|
||||
CopyMemory(buff, globlemem, size);
|
||||
if (buff)
|
||||
{
|
||||
CopyMemory(buff, globlemem, size);
|
||||
rc = ERROR_SUCCESS;
|
||||
}
|
||||
else
|
||||
{
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
GlobalUnlock(hClipdata);
|
||||
CloseClipboard();
|
||||
rc = ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2545,7 +2697,7 @@ exit:
|
||||
response.requestedFormatData = (BYTE *)buff;
|
||||
if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response))
|
||||
{
|
||||
// CAUTION: if failed to send, server will wait a long time
|
||||
// CAUTION: if failed to send, server will wait a long time, default 30 seconds.
|
||||
}
|
||||
|
||||
if (buff)
|
||||
@@ -2621,9 +2773,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
|
||||
rc = CHANNEL_RC_OK;
|
||||
} while (0);
|
||||
|
||||
if (!SetEvent(clipboard->response_data_event))
|
||||
if (!SetEvent(clipboard->formatDataRespEvent))
|
||||
{
|
||||
// CAUTION: critical error here, process will hang up until wait timeout default 3min.
|
||||
// If failed to set event, set flag to indicate the event is received.
|
||||
DEBUG_CLIPRDR("wf_cliprdr_server_format_data_response(), SetEvent failed with 0x%x", GetLastError());
|
||||
clipboard->formatDataRespReceived = TRUE;
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
return rc;
|
||||
@@ -2899,7 +3053,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
|
||||
if (!SetEvent(clipboard->req_fevent))
|
||||
{
|
||||
// CAUTION: critical error here, process will hang up until wait timeout default 3min.
|
||||
// If failed to set event, set flag to indicate the event is received.
|
||||
DEBUG_CLIPRDR("wf_cliprdr_server_file_contents_response(), SetEvent failed with 0x%x", GetLastError());
|
||||
clipboard->req_f_received = TRUE;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
@@ -2934,14 +3090,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
|
||||
(formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping))))
|
||||
goto error;
|
||||
|
||||
if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
||||
if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
||||
goto error;
|
||||
clipboard->formatDataRespReceived = FALSE;
|
||||
|
||||
if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex")))
|
||||
goto error;
|
||||
|
||||
if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL)))
|
||||
goto error;
|
||||
clipboard->req_f_received = FALSE;
|
||||
|
||||
if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL)))
|
||||
goto error;
|
||||
@@ -3002,8 +3160,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
|
||||
clipboard->data_obj = NULL;
|
||||
}
|
||||
|
||||
if (clipboard->response_data_event)
|
||||
CloseHandle(clipboard->response_data_event);
|
||||
if (clipboard->formatDataRespEvent)
|
||||
CloseHandle(clipboard->formatDataRespEvent);
|
||||
|
||||
if (clipboard->data_obj_mutex)
|
||||
CloseHandle(clipboard->data_obj_mutex);
|
||||
|
||||
@@ -23,7 +23,7 @@ serde = { version = "1.0", optional = true }
|
||||
serde_derive = { version = "1.0", optional = true }
|
||||
log = "0.4"
|
||||
rdev = { git = "https://github.com/rustdesk-org/rdev" }
|
||||
tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller" }
|
||||
tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller", branch = "history/rebase_upstream_20240722" }
|
||||
hbb_common = { path = "../hbb_common" }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3;
|
||||
const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31;
|
||||
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.
|
||||
pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100;
|
||||
|
||||
@@ -226,14 +229,24 @@ impl MouseControllable for Enigo {
|
||||
}
|
||||
self.last_click_time = Some(now);
|
||||
let (current_x, current_y) = Self::mouse_location();
|
||||
let (button, event_type) = match button {
|
||||
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown),
|
||||
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown),
|
||||
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown),
|
||||
let (button, event_type, btn_value) = match button {
|
||||
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, None),
|
||||
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, None),
|
||||
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);
|
||||
return Ok(());
|
||||
},
|
||||
}
|
||||
};
|
||||
let dest = CGPoint::new(current_x as f64, current_y as f64);
|
||||
if let Some(src) = self.event_source.as_ref() {
|
||||
@@ -244,6 +257,9 @@ impl MouseControllable for Enigo {
|
||||
self.multiple_click,
|
||||
);
|
||||
}
|
||||
if let Some(v) = btn_value {
|
||||
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v);
|
||||
}
|
||||
self.post(event);
|
||||
}
|
||||
}
|
||||
@@ -252,14 +268,24 @@ impl MouseControllable for Enigo {
|
||||
|
||||
fn mouse_up(&mut self, button: MouseButton) {
|
||||
let (current_x, current_y) = Self::mouse_location();
|
||||
let (button, event_type) = match button {
|
||||
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp),
|
||||
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp),
|
||||
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp),
|
||||
let (button, event_type, btn_value) = match button {
|
||||
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp, None),
|
||||
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp, None),
|
||||
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);
|
||||
return;
|
||||
},
|
||||
}
|
||||
};
|
||||
let dest = CGPoint::new(current_x as f64, current_y as f64);
|
||||
if let Some(src) = self.event_source.as_ref() {
|
||||
@@ -270,6 +296,9 @@ impl MouseControllable for Enigo {
|
||||
self.multiple_click,
|
||||
);
|
||||
}
|
||||
if let Some(v) = btn_value {
|
||||
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v);
|
||||
}
|
||||
self.post(event);
|
||||
}
|
||||
}
|
||||
@@ -345,7 +374,7 @@ impl KeyboardControllable for Enigo {
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
fn key_sequence(&mut self, sequence: &str) {
|
||||
// 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
|
||||
@@ -382,12 +411,10 @@ impl KeyboardControllable for Enigo {
|
||||
fn key_down(&mut self, key: Key) -> crate::ResultType {
|
||||
let code = self.key_to_keycode(key);
|
||||
if code == u16::MAX {
|
||||
return Err("".into());
|
||||
return Err("".into());
|
||||
}
|
||||
if let Some(src) = self.event_source.as_ref() {
|
||||
if let Ok(event) =
|
||||
CGEvent::new_keyboard_event(src.clone(), code, true)
|
||||
{
|
||||
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) {
|
||||
self.post(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ libc = "0.2"
|
||||
dlopen = "0.1"
|
||||
toml = "0.7"
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052
|
||||
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" }
|
||||
# new sysinfo issue: https://github.com/rustdesk/rustdesk/pull/6330#issuecomment-2270871442
|
||||
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo", branch = "rlim_max" }
|
||||
thiserror = "1.0"
|
||||
httparse = "1.5"
|
||||
base64 = "0.22"
|
||||
@@ -46,7 +46,7 @@ url = "2.2"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
mac_address = "1.1"
|
||||
machine-uid = { git = "https://github.com/21pages/machine-uid" }
|
||||
machine-uid = { git = "https://github.com/rustdesk-org/machine-uid" }
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false }
|
||||
rustls-platform-verifier = "0.3.1"
|
||||
|
||||
@@ -81,10 +81,13 @@ message LoginRequest {
|
||||
uint64 session_id = 10;
|
||||
string version = 11;
|
||||
OSLogin os_login = 12;
|
||||
string my_platform = 13;
|
||||
bytes hwid = 14;
|
||||
}
|
||||
|
||||
message Auth2FA {
|
||||
string code = 1;
|
||||
bytes hwid = 2;
|
||||
}
|
||||
|
||||
message ChatMessage { string text = 1; }
|
||||
@@ -136,6 +139,7 @@ message LoginResponse {
|
||||
string error = 1;
|
||||
PeerInfo peer_info = 2;
|
||||
}
|
||||
bool enable_trusted_devices = 3;
|
||||
}
|
||||
|
||||
message TouchScaleUpdate {
|
||||
@@ -315,13 +319,28 @@ message Hash {
|
||||
string challenge = 2;
|
||||
}
|
||||
|
||||
enum ClipboardFormat {
|
||||
Text = 0;
|
||||
Rtf = 1;
|
||||
Html = 2;
|
||||
ImageRgba = 21;
|
||||
ImagePng = 22;
|
||||
ImageSvg = 23;
|
||||
Special = 31;
|
||||
}
|
||||
|
||||
message Clipboard {
|
||||
bool compress = 1;
|
||||
bytes content = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
ClipboardFormat format = 5;
|
||||
// Special format name, only used when format is Special.
|
||||
string special_name = 6;
|
||||
}
|
||||
|
||||
message MultiClipboards { repeated Clipboard clipboards = 1; }
|
||||
|
||||
enum FileType {
|
||||
Dir = 0;
|
||||
DirLink = 2;
|
||||
@@ -355,6 +374,12 @@ message ReadAllFiles {
|
||||
bool include_hidden = 3;
|
||||
}
|
||||
|
||||
message FileRename {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
string new_name = 3;
|
||||
}
|
||||
|
||||
message FileAction {
|
||||
oneof union {
|
||||
ReadDir read_dir = 1;
|
||||
@@ -366,6 +391,7 @@ message FileAction {
|
||||
ReadAllFiles all_files = 7;
|
||||
FileTransferCancel cancel = 8;
|
||||
FileTransferSendConfirmRequest send_confirm = 9;
|
||||
FileRename rename = 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,5 +842,6 @@ message Message {
|
||||
PeerInfo peer_info = 25;
|
||||
PointerDeviceEvent pointer_device_event = 26;
|
||||
Auth2FA auth_2fa = 27;
|
||||
MultiClipboards multi_clipboards = 28;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use bytes::Bytes;
|
||||
use rand::Rng;
|
||||
use regex::Regex;
|
||||
use serde as de;
|
||||
@@ -38,7 +39,7 @@ pub const REG_INTERVAL: i64 = 15_000;
|
||||
pub const COMPRESS_LEVEL: i32 = 3;
|
||||
const SERIAL: i32 = 3;
|
||||
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")]
|
||||
lazy_static::lazy_static! {
|
||||
@@ -52,6 +53,7 @@ lazy_static::lazy_static! {
|
||||
static ref CONFIG: RwLock<Config> = RwLock::new(Config::load());
|
||||
static ref CONFIG2: RwLock<Config2> = RwLock::new(Config2::load());
|
||||
static ref LOCAL_CONFIG: RwLock<LocalConfig> = RwLock::new(LocalConfig::load());
|
||||
static ref TRUSTED_DEVICES: RwLock<(Vec<TrustedDevice>, bool)> = Default::default();
|
||||
static ref ONLINE: Mutex<HashMap<String, i64>> = Default::default();
|
||||
pub static ref PROD_RENDEZVOUS_SERVER: RwLock<String> = RwLock::new(match option_env!("RENDEZVOUS_SERVER") {
|
||||
Some(key) if !key.is_empty() => key,
|
||||
@@ -69,7 +71,7 @@ lazy_static::lazy_static! {
|
||||
pub static ref DEFAULT_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||
pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||
pub static ref HARD_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||
pub static ref BUILDIN_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||
pub static ref BUILTIN_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -208,6 +210,10 @@ pub struct Config2 {
|
||||
nat_type: i32,
|
||||
#[serde(default, deserialize_with = "deserialize_i32")]
|
||||
serial: i32,
|
||||
#[serde(default, deserialize_with = "deserialize_string")]
|
||||
unlock_pin: String,
|
||||
#[serde(default, deserialize_with = "deserialize_string")]
|
||||
trusted_devices: String,
|
||||
|
||||
#[serde(default)]
|
||||
socks: Option<Socks5Server>,
|
||||
@@ -290,6 +296,8 @@ pub struct PeerConfig {
|
||||
pub keyboard_mode: String,
|
||||
#[serde(flatten)]
|
||||
pub view_only: ViewOnly,
|
||||
#[serde(flatten)]
|
||||
pub sync_init_clipboard: SyncInitClipboard,
|
||||
// Mouse wheel or touchpad scroll mode
|
||||
#[serde(
|
||||
default = "PeerConfig::default_reverse_mouse_wheel",
|
||||
@@ -367,6 +375,7 @@ impl Default for PeerConfig {
|
||||
ui_flutter: Default::default(),
|
||||
info: Default::default(),
|
||||
transfer: Default::default(),
|
||||
sync_init_clipboard: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,14 +436,20 @@ fn patch(path: PathBuf) -> PathBuf {
|
||||
impl Config2 {
|
||||
fn load() -> Config2 {
|
||||
let mut config = Config::load_::<Config2>("2");
|
||||
let mut store = false;
|
||||
if let Some(mut socks) = config.socks {
|
||||
let (password, _, store) =
|
||||
let (password, _, store2) =
|
||||
decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
|
||||
socks.password = password;
|
||||
config.socks = Some(socks);
|
||||
if store {
|
||||
config.store();
|
||||
}
|
||||
store |= store2;
|
||||
}
|
||||
let (unlock_pin, _, store2) =
|
||||
decrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION);
|
||||
config.unlock_pin = unlock_pin;
|
||||
store |= store2;
|
||||
if store {
|
||||
config.store();
|
||||
}
|
||||
config
|
||||
}
|
||||
@@ -450,6 +465,8 @@ impl Config2 {
|
||||
encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
|
||||
config.socks = Some(socks);
|
||||
}
|
||||
config.unlock_pin =
|
||||
encrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
|
||||
Config::store_(&config, "2");
|
||||
}
|
||||
|
||||
@@ -988,6 +1005,7 @@ impl Config {
|
||||
}
|
||||
config.password = password.into();
|
||||
config.store();
|
||||
Self::clear_trusted_devices();
|
||||
}
|
||||
|
||||
pub fn get_permanent_password() -> String {
|
||||
@@ -1081,6 +1099,77 @@ impl Config {
|
||||
NetworkType::Direct
|
||||
}
|
||||
|
||||
pub fn get_unlock_pin() -> String {
|
||||
CONFIG2.read().unwrap().unlock_pin.clone()
|
||||
}
|
||||
|
||||
pub fn set_unlock_pin(pin: &str) {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
if pin == config.unlock_pin {
|
||||
return;
|
||||
}
|
||||
config.unlock_pin = pin.to_string();
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_trusted_devices_json() -> String {
|
||||
serde_json::to_string(&Self::get_trusted_devices()).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_trusted_devices() -> Vec<TrustedDevice> {
|
||||
let (devices, synced) = TRUSTED_DEVICES.read().unwrap().clone();
|
||||
if synced {
|
||||
return devices;
|
||||
}
|
||||
let devices = CONFIG2.read().unwrap().trusted_devices.clone();
|
||||
let (devices, succ, store) = decrypt_str_or_original(&devices, PASSWORD_ENC_VERSION);
|
||||
if succ {
|
||||
let mut devices: Vec<TrustedDevice> =
|
||||
serde_json::from_str(&devices).unwrap_or_default();
|
||||
let len = devices.len();
|
||||
devices.retain(|d| !d.outdate());
|
||||
if store || devices.len() != len {
|
||||
Self::set_trusted_devices(devices.clone());
|
||||
}
|
||||
*TRUSTED_DEVICES.write().unwrap() = (devices.clone(), true);
|
||||
devices
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_trusted_devices(mut trusted_devices: Vec<TrustedDevice>) {
|
||||
trusted_devices.retain(|d| !d.outdate());
|
||||
let devices = serde_json::to_string(&trusted_devices).unwrap_or_default();
|
||||
let max_len = 1024 * 1024;
|
||||
if devices.bytes().len() > max_len {
|
||||
log::error!("Trusted devices too large: {}", devices.bytes().len());
|
||||
return;
|
||||
}
|
||||
let devices = encrypt_str_or_original(&devices, PASSWORD_ENC_VERSION, max_len);
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
config.trusted_devices = devices;
|
||||
config.store();
|
||||
*TRUSTED_DEVICES.write().unwrap() = (trusted_devices, true);
|
||||
}
|
||||
|
||||
pub fn add_trusted_device(device: TrustedDevice) {
|
||||
let mut devices = Self::get_trusted_devices();
|
||||
devices.retain(|d| d.hwid != device.hwid);
|
||||
devices.push(device);
|
||||
Self::set_trusted_devices(devices);
|
||||
}
|
||||
|
||||
pub fn remove_trusted_devices(hwids: &Vec<Bytes>) {
|
||||
let mut devices = Self::get_trusted_devices();
|
||||
devices.retain(|d| !hwids.contains(&d.hwid));
|
||||
Self::set_trusted_devices(devices);
|
||||
}
|
||||
|
||||
pub fn clear_trusted_devices() {
|
||||
Self::set_trusted_devices(Default::default());
|
||||
}
|
||||
|
||||
pub fn get() -> Config {
|
||||
return CONFIG.read().unwrap().clone();
|
||||
}
|
||||
@@ -1376,6 +1465,13 @@ serde_field_bool!(
|
||||
"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)]
|
||||
pub struct LocalConfig {
|
||||
#[serde(default, deserialize_with = "deserialize_string")]
|
||||
@@ -1911,6 +2007,22 @@ impl Group {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct TrustedDevice {
|
||||
pub hwid: Bytes,
|
||||
pub time: i64,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub platform: String,
|
||||
}
|
||||
|
||||
impl TrustedDevice {
|
||||
pub fn outdate(&self) -> bool {
|
||||
const DAYS_90: i64 = 90 * 24 * 60 * 60 * 1000;
|
||||
self.time + DAYS_90 < crate::get_time()
|
||||
}
|
||||
}
|
||||
|
||||
deserialize_default!(deserialize_string, String);
|
||||
deserialize_default!(deserialize_bool, bool);
|
||||
deserialize_default!(deserialize_i32, i32);
|
||||
@@ -2054,6 +2166,7 @@ pub mod keys {
|
||||
pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality";
|
||||
pub const OPTION_CUSTOM_FPS: &str = "custom-fps";
|
||||
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_LANGUAGE: &str = "lang";
|
||||
pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left";
|
||||
@@ -2100,6 +2213,7 @@ pub mod keys {
|
||||
pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture";
|
||||
pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str =
|
||||
"enable-android-software-encoding-half-scale";
|
||||
pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices";
|
||||
|
||||
// buildin options
|
||||
pub const OPTION_DISPLAY_NAME: &str = "display-name";
|
||||
@@ -2114,6 +2228,10 @@ pub mod keys {
|
||||
pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card";
|
||||
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
|
||||
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
|
||||
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
|
||||
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
||||
@@ -2172,6 +2290,7 @@ pub mod keys {
|
||||
OPTION_CUSTOM_IMAGE_QUALITY,
|
||||
OPTION_CUSTOM_FPS,
|
||||
OPTION_CODEC_PREFERENCE,
|
||||
OPTION_SYNC_INIT_CLIPBOARD,
|
||||
];
|
||||
// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS
|
||||
pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
|
||||
@@ -2240,6 +2359,7 @@ pub mod keys {
|
||||
OPTION_PRESET_ADDRESS_BOOK_TAG,
|
||||
OPTION_ENABLE_DIRECTX_CAPTURE,
|
||||
OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE,
|
||||
OPTION_ENABLE_TRUSTED_DEVICES,
|
||||
];
|
||||
|
||||
// BUILDIN_SETTINGS
|
||||
@@ -2256,6 +2376,10 @@ pub mod keys {
|
||||
OPTION_HIDE_USERNAME_ON_CARD,
|
||||
OPTION_HIDE_HELP_CARDS,
|
||||
OPTION_DEFAULT_CONNECT_PASSWORD,
|
||||
OPTION_HIDE_TRAY,
|
||||
OPTION_ONE_WAY_CLIPBOARD_REDIRECTION,
|
||||
OPTION_ALLOW_LOGON_SCREEN_PASSWORD,
|
||||
OPTION_ONE_WAY_FILE_TRANSFER,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -838,6 +838,21 @@ pub fn create_dir(dir: &str) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> {
|
||||
let path = std::path::Path::new(&path);
|
||||
if path.exists() {
|
||||
let dir = path
|
||||
.parent()
|
||||
.ok_or(anyhow!("Parent directoy of {path:?} not exists"))?;
|
||||
let new_path = dir.join(&new_name);
|
||||
std::fs::rename(&path, &new_path)?;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("{path:?} not exists");
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
|
||||
for entry in entries {
|
||||
|
||||
@@ -89,11 +89,11 @@ pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String
|
||||
log::error!("Duplicate encryption!");
|
||||
return s.to_owned();
|
||||
}
|
||||
if s.bytes().len() > max_len {
|
||||
if s.chars().count() > max_len {
|
||||
return String::default();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec<u
|
||||
return vec![];
|
||||
}
|
||||
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();
|
||||
version.append(&mut s.into_bytes());
|
||||
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())
|
||||
}
|
||||
|
||||
fn encrypt(v: &[u8], max_len: usize) -> Result<String, ()> {
|
||||
if !v.is_empty() && v.len() <= max_len {
|
||||
fn encrypt(v: &[u8]) -> Result<String, ()> {
|
||||
if !v.is_empty() {
|
||||
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
|
||||
} else {
|
||||
Err(())
|
||||
|
||||
@@ -7,6 +7,9 @@ lazy_static::lazy_static! {
|
||||
|
||||
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
|
||||
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 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]
|
||||
pub fn is_gdm_user(username: &str) -> bool {
|
||||
username == "gdm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.2.7"
|
||||
version = "1.3.1"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ cfg-if = "1.0"
|
||||
num_cpus = "1.15"
|
||||
lazy_static = "1.4"
|
||||
hbb_common = { path = "../hbb_common" }
|
||||
webm = { git = "https://github.com/21pages/rust-webm" }
|
||||
webm = { git = "https://github.com/rustdesk-org/rust-webm" }
|
||||
serde = {version="1.0", features=["derive"]}
|
||||
|
||||
[dependencies.winapi]
|
||||
@@ -59,7 +59,7 @@ gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
|
||||
gstreamer-video = { version = "0.16", optional = true }
|
||||
|
||||
[dependencies.hwcodec]
|
||||
git = "https://github.com/21pages/hwcodec"
|
||||
git = "https://github.com/rustdesk-org/hwcodec"
|
||||
optional = true
|
||||
|
||||
|
||||
|
||||
@@ -188,6 +188,52 @@ fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &s
|
||||
generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file, regex);
|
||||
}
|
||||
|
||||
// If you have problems installing ffmpeg, you can download $VCPKG_ROOT/installed from ci
|
||||
// Linux require link in hwcodec
|
||||
/*
|
||||
fn ffmpeg() {
|
||||
// ffmpeg
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||
let static_libs = vec!["avcodec", "avutil", "avformat"];
|
||||
static_libs.iter().for_each(|lib| {
|
||||
find_package(lib);
|
||||
});
|
||||
if target_os == "windows" {
|
||||
println!("cargo:rustc-link-lib=static=libmfx");
|
||||
}
|
||||
|
||||
// os
|
||||
let dyn_libs: Vec<&str> = if target_os == "windows" {
|
||||
["User32", "bcrypt", "ole32", "advapi32"].to_vec()
|
||||
} else if target_os == "linux" {
|
||||
let mut v = ["va", "va-drm", "va-x11", "vdpau", "X11", "stdc++"].to_vec();
|
||||
if target_arch == "x86_64" {
|
||||
v.push("z");
|
||||
}
|
||||
v
|
||||
} else if target_os == "macos" || target_os == "ios" {
|
||||
["c++", "m"].to_vec()
|
||||
} else if target_os == "android" {
|
||||
["z", "m", "android", "atomic"].to_vec()
|
||||
} else {
|
||||
panic!("unsupported os");
|
||||
};
|
||||
dyn_libs
|
||||
.iter()
|
||||
.map(|lib| println!("cargo:rustc-link-lib={}", lib))
|
||||
.count();
|
||||
|
||||
if target_os == "macos" || target_os == "ios" {
|
||||
println!("cargo:rustc-link-lib=framework=CoreFoundation");
|
||||
println!("cargo:rustc-link-lib=framework=CoreVideo");
|
||||
println!("cargo:rustc-link-lib=framework=CoreMedia");
|
||||
println!("cargo:rustc-link-lib=framework=VideoToolbox");
|
||||
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
fn main() {
|
||||
// note: all link symbol names in x86 (32-bit) are prefixed wth "_".
|
||||
// run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc,
|
||||
@@ -204,6 +250,7 @@ fn main() {
|
||||
gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*");
|
||||
gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*");
|
||||
gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*");
|
||||
// ffmpeg();
|
||||
|
||||
// there is problem with cfg(target_os) in build.rs, so use our workaround
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
|
||||
@@ -349,6 +349,10 @@ fn init_ndk_context() -> JniResult<()> {
|
||||
jvm.get_java_vm_pointer() as _,
|
||||
ctx.as_obj().as_raw() as _,
|
||||
);
|
||||
#[cfg(feature = "hwcodec")]
|
||||
hwcodec::android::ffmpeg_set_java_vm(
|
||||
jvm.get_java_vm_pointer() as _,
|
||||
);
|
||||
}
|
||||
*lock = true;
|
||||
return Ok(());
|
||||
|
||||
@@ -498,6 +498,15 @@ pub struct HwCodecConfig {
|
||||
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
|
||||
// install: --server start check process, check process send to --server, ui get from --server
|
||||
// portable: ui start check process, check process send to ui
|
||||
@@ -509,7 +518,12 @@ impl HwCodecConfig {
|
||||
log::info!("set hwcodec config");
|
||||
log::debug!("{config:?}");
|
||||
#[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_SET_BY_IPC.lock().unwrap() = true;
|
||||
}
|
||||
@@ -587,7 +601,8 @@ impl HwCodecConfig {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
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();
|
||||
if c.signature == new_signature {
|
||||
log::debug!("load cached hwcodec config: {c:?}");
|
||||
|
||||
@@ -316,7 +316,7 @@ impl ToString for CodecFormat {
|
||||
CodecFormat::AV1 => "AV1".into(),
|
||||
CodecFormat::H264 => "H264".into(),
|
||||
CodecFormat::H265 => "H265".into(),
|
||||
CodecFormat::Unknown => "Unknow".into(),
|
||||
CodecFormat::Unknown => "Unknown".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user