332 Commits

257 changed files with 14383 additions and 5808 deletions

View File

@@ -1,8 +1,14 @@
[target.x86_64-pc-windows-msvc] [target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"] rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc] [target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
[target.'cfg(target_os="macos")'] [target.'cfg(target_os="macos")']
rustflags = [ rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
] ]
#[target.'cfg(target_os="linux")']
# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change
# this is unlikely to help also, because the other so files still use libc dynamically
#rustflags = [
# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic"
#]

View File

@@ -6,7 +6,7 @@ on:
workflow_call: workflow_call:
env: env:
FLUTTER_VERSION: "3.16.9" FLUTTER_VERSION: "3.19.6"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503

View File

@@ -4,9 +4,9 @@ env:
# MIN_SUPPORTED_RUST_VERSION: "1.46.0" # MIN_SUPPORTED_RUST_VERSION: "1.46.0"
# CICD_INTERMEDIATES_DIR: "_cicd-intermediates" # CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19 # vcpkg version: 2024.06.15
# for multiarch gcc compatibility # for multiarch gcc compatibility
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50" VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
on: on:
workflow_dispatch: workflow_dispatch:
@@ -112,6 +112,8 @@ jobs:
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \ libgtk-3-dev \
libpulse-dev \ libpulse-dev \
libva-dev \
libvdpau-dev \
libxcb-randr0-dev \ libxcb-randr0-dev \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \

View File

@@ -19,7 +19,12 @@ jobs:
steps: steps:
- name: Generate RustDesk version file - name: Generate RustDesk version file
run: | 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)" 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 "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt

View File

@@ -18,9 +18,11 @@ on:
# in this file! # in this file!
env: 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 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" CARGO_NDK_VERSION: "3.1.2"
SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
SCITER_NASM_DEBVERSION: "2.14-1"
LLVM_VERSION: "15.0.6" LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.19.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). 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" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}" TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15 # vcpkg version: 2024.07.12
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
VERSION: "1.2.7" VERSION: "1.3.2"
NDK_VERSION: "r26d" NDK_VERSION: "r27b"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
@@ -56,7 +58,7 @@ jobs:
fail-fast: false fail-fast: false
build-for-windows-flutter: build-for-windows-flutter:
name: ${{ matrix.job.target }} name: ${{ matrix.job.target }}
needs: [build-RustDeskTempTopMostWindow] needs: [build-RustDeskTempTopMostWindow]
runs-on: ${{ matrix.job.os }} runs-on: ${{ matrix.job.os }}
strategy: strategy:
@@ -65,7 +67,12 @@ jobs:
job: job:
# - { target: i686-pc-windows-msvc , os: windows-2022 } # - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: x86_64-pc-windows-gnu , 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 } # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
steps: steps:
- name: Export GitHub Actions cache environment variables - name: Export GitHub Actions cache environment variables
@@ -93,7 +100,7 @@ jobs:
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1 uses: dtolnay/rust-toolchain@v1
with: with:
toolchain: ${{ env.WIN_RUST_VERSION }} toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }} targets: ${{ matrix.job.target }}
components: "rustfmt" components: "rustfmt"
@@ -113,10 +120,25 @@ jobs:
with: with:
vcpkgDirectory: C:\vcpkg vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
env:
VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }}
run: | 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 shell: bash
- name: Build rustdesk - name: Build rustdesk
@@ -156,7 +178,7 @@ jobs:
if: env.UPLOAD_ARTIFACT == 'true' if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@master uses: actions/upload-artifact@master
with: with:
name: rustdesk-unsigned-windows-${{ matrix.job.arch }} name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: rustdesk path: rustdesk
- name: Sign rustdesk files - name: Sign rustdesk files
@@ -219,7 +241,12 @@ jobs:
job: job:
# - { target: i686-pc-windows-msvc , os: windows-2022 } # - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: x86_64-pc-windows-gnu , 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 } # - { target: aarch64-pc-windows-msvc, os: windows-2022 }
steps: steps:
- name: Export GitHub Actions cache environment variables - name: Export GitHub Actions cache environment variables
@@ -230,7 +257,7 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install LLVM and Clang - name: Install LLVM and Clang
uses: rustdesk-org/install-llvm-action-32bit@master uses: rustdesk-org/install-llvm-action-32bit@master
@@ -253,10 +280,25 @@ jobs:
with: with:
vcpkgDirectory: C:\vcpkg vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
env:
VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }}
run: | 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 shell: bash
- name: Build rustdesk - 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 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, # 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. # 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 mv ./usbmmidd_v2 ./Release || true
- name: find Runner.res - name: find Runner.res
@@ -341,7 +383,7 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install flutter rust bridge deps - name: Install flutter rust bridge deps
shell: bash shell: bash
@@ -424,9 +466,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
brew install nasm brew install nasm yasm
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install flutter - name: Install flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
@@ -437,10 +479,23 @@ jobs:
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@v11
with: with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
run: | 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 shell: bash
- name: Install Rust toolchain - name: Install Rust toolchain
@@ -506,7 +561,7 @@ jobs:
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code - 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" # $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 || ''); core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Import the codesign cert - name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null if: env.MACOS_P12_BASE64 != null
@@ -651,10 +706,22 @@ jobs:
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@v11
with: with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
run: | 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) - name: Show version information (Rust, cargo, Clang)
shell: bash shell: bash
@@ -770,15 +837,35 @@ jobs:
arch: aarch64, arch: aarch64,
target: aarch64-linux-android, target: aarch64-linux-android,
os: ubuntu-20.04, os: ubuntu-20.04,
openssl-arch: android-arm64, reltype: release,
suffix: "",
} }
- { - {
arch: armv7, arch: armv7,
target: armv7-linux-androideabi, target: armv7-linux-androideabi,
os: ubuntu-20.04, 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: 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 - name: Export GitHub Actions cache environment variables
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
@@ -797,7 +884,7 @@ jobs:
git \ git \
g++ \ g++ \
g++-multilib \ g++-multilib \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-10-dev \ libclang-10-dev \
@@ -822,7 +909,7 @@ jobs:
wget wget
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install flutter - name: Install flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
@@ -839,17 +926,34 @@ jobs:
with: with:
vcpkgDirectory: /opt/artifacts/vcpkg vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
run: | run: |
case ${{ matrix.job.target }} in case ${{ matrix.job.target }} in
aarch64-linux-android) aarch64-linux-android)
./flutter/build_android_deps.sh arm64-v8a ANDROID_TARGET=arm64-v8a
;; ;;
armv7-linux-androideabi) 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 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 shell: bash
- name: Restore bridge files - name: Restore bridge files
@@ -872,8 +976,11 @@ jobs:
- name: fix android for flutter 3.13 - name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: | run: |
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml cd flutter
cd flutter/lib sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
flutter pub get
cd lib
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk lib - name: Build rustdesk lib
@@ -894,8 +1001,24 @@ jobs:
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a 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 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 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 - name: Build rustdesk
shell: bash shell: bash
env: env:
@@ -911,8 +1034,8 @@ jobs:
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
# build flutter # build flutter
pushd flutter pushd flutter
flutter build apk --release --target-platform android-arm64 --split-per-abi flutter build apk "--${{ matrix.job.reltype }}" --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 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) armv7-linux-androideabi)
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a 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 cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
# build flutter # build flutter
pushd flutter pushd flutter
flutter build apk --release --target-platform android-arm --split-per-abi flutter build apk "--${{ matrix.job.reltype }}" --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 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 esac
popd popd
mkdir -p signed-apk; pushd signed-apk 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 - uses: r0adkll/sign-android-release@v1
name: Sign app APK name: Sign app APK
@@ -967,9 +1108,185 @@ jobs:
files: | files: |
signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk 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: build-rustdesk-linux:
needs: [generate-bridge-linux] needs: [generate-bridge-linux]
name: build rustdesk linux ${{ matrix.job.target }} name: build rustdesk linux ${{ matrix.job.target }}
runs-on: ${{ matrix.job.on }} runs-on: ${{ matrix.job.on }}
strategy: strategy:
fail-fast: false fail-fast: false
@@ -1010,7 +1327,7 @@ jobs:
sudo apt-get install -y nasm qemu-user-static sudo apt-get install -y nasm qemu-user-static
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set Swap Space - name: Set Swap Space
if: ${{ matrix.job.arch == 'x86_64' }} if: ${{ matrix.job.arch == 'x86_64' }}
@@ -1054,11 +1371,24 @@ jobs:
with: with:
vcpkgDirectory: /opt/artifacts/vcpkg vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies - name: Install vcpkg dependencies
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
run: | 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 shell: bash
- name: Restore bridge files - name: Restore bridge files
@@ -1094,7 +1424,7 @@ jobs:
gcc \ gcc \
git \ git \
g++ \ g++ \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libclang-10-dev \ libclang-10-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
@@ -1237,16 +1567,16 @@ jobs:
rustdesk-*.deb rustdesk-*.deb
rustdesk-*.rpm rustdesk-*.rpm
- name: Upload deb - name: Upload deb
uses: actions/upload-artifact@master uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true' if: env.UPLOAD_ARTIFACT == 'true'
with: with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
path: 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 # 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 # 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 - name: Patch archlinux PKGBUILD
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
run: | run: |
@@ -1290,6 +1620,7 @@ jobs:
deb_arch: amd64, deb_arch: amd64,
sciter_arch: x64, sciter_arch: x64,
vcpkg-triplet: x64-linux, vcpkg-triplet: x64-linux,
extra_features: ",hwcodec",
} }
- { - {
arch: armv7, arch: armv7,
@@ -1299,6 +1630,7 @@ jobs:
deb_arch: armhf, deb_arch: armhf,
sciter_arch: arm32, sciter_arch: arm32,
vcpkg-triplet: arm-linux, vcpkg-triplet: arm-linux,
extra_features: "",
} }
steps: steps:
- name: Export GitHub Actions cache environment variables - 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_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); 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 - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Free Space - name: Free Space
run: | run: |
@@ -1336,7 +1651,7 @@ jobs:
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1 uses: dtolnay/rust-toolchain@v1
with: with:
toolchain: ${{ env.RUST_VERSION }} toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }} targets: ${{ matrix.job.target }}
components: "rustfmt" components: "rustfmt"
@@ -1345,18 +1660,6 @@ jobs:
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}') RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV 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 - uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk sciter binary for ${{ matrix.job.arch }} name: Build rustdesk sciter binary for ${{ matrix.job.arch }}
id: vcpkg id: vcpkg
@@ -1368,19 +1671,17 @@ jobs:
ls -l "${PWD}" ls -l "${PWD}"
dockerRunArgs: | dockerRunArgs: |
--volume "${PWD}:/workspace" --volume "${PWD}:/workspace"
--volume "/opt/artifacts:/opt/artifacts"
shell: /bin/bash shell: /bin/bash
install: | install: |
apt-get update -y apt-get update
apt-get install -y \ apt-get install -y \
build-essential \ build-essential \
clang \ clang \
cmake \
curl \ curl \
gcc \ gcc \
git \ git \
g++ \ g++ \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libclang-dev \ libclang-dev \
libdbus-1-dev \ libdbus-1-dev \
@@ -1398,39 +1699,93 @@ jobs:
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
libxdo-dev \ libxdo-dev \
libxfixes-dev \ libxfixes-dev \
nasm \
ninja-build \ ninja-build \
pkg-config \ pkg-config \
python3 \ python3 \
python3.7 \
rpm \ rpm \
unzip \ unzip \
wget \ wget \
xz-utils xz-utils \
run: | zip
# disable git safe.directory # arm-linux needs CMake and vcokg built from source as there
git config --global --add safe.directory "*" # 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 # rust
pushd /opt pushd /opt
# do not use rustup, because memory overflow in qemu # 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 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 }} 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/ mkdir -p ~/.cargo/
echo """ echo """
[source.crates-io] [source.crates-io]
registry = 'https://github.com/rust-lang/crates.io-index' registry = 'https://github.com/rust-lang/crates.io-index'
""" > ~/.cargo/config """ > ~/.cargo/config
cat ~/.cargo/config cat ~/.cargo/config
# install dependencies from vcpkg
# build
pushd /workspace
python3 ./res/inline-sciter.py
export VCPKG_ROOT=/opt/artifacts/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 export CARGO_INCREMENTAL=0
cargo build --features inline --release --bins --jobs 1 cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
# package # make debian package
mkdir -p ./Release mkdir -p ./Release
mv ./target/release/rustdesk ./Release/rustdesk 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 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: | files: |
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
- name: Upload deb - name: Upload deb
uses: actions/upload-artifact@master uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true' if: env.UPLOAD_ARTIFACT == 'true'
with: with:
@@ -1470,17 +1825,11 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
job: job:
- { - { target: x86_64-unknown-linux-gnu, arch: x86_64 }
target: x86_64-unknown-linux-gnu, - { target: aarch64-unknown-linux-gnu, arch: aarch64 }
arch: x86_64,
}
- {
target: aarch64-unknown-linux-gnu,
arch: aarch64,
}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Download Binary - name: Download Binary
uses: actions/download-artifact@master uses: actions/download-artifact@master
@@ -1519,7 +1868,7 @@ jobs:
build-flatpak: build-flatpak:
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }} name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
needs: needs:
- build-rustdesk-linux - build-rustdesk-linux
- build-rustdesk-linux-sciter - build-rustdesk-linux-sciter
runs-on: ${{ matrix.job.on }} runs-on: ${{ matrix.job.on }}
@@ -1552,7 +1901,7 @@ jobs:
} }
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Download Binary - name: Download Binary
uses: actions/download-artifact@master uses: actions/download-artifact@master
@@ -1587,7 +1936,7 @@ jobs:
# disable git safe.directory # disable git safe.directory
git config --global --add safe.directory "*" git config --global --add safe.directory "*"
pushd /workspace pushd /workspace
# install # install
apt-get update -y apt-get update -y
apt-get install -y \ apt-get install -y \
cmake \ cmake \
@@ -1628,7 +1977,7 @@ jobs:
RELEASE_NAME: web-basic RELEASE_NAME: web-basic
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Prepare env - name: Prepare env
run: | run: |

View File

@@ -18,7 +18,7 @@ env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15 # vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.2.7" VERSION: "1.3.2"
NDK_VERSION: "r26d" NDK_VERSION: "r26d"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -262,7 +262,7 @@ jobs:
git \ git \
g++ \ g++ \
g++-multilib \ g++-multilib \
libappindicator3-dev \ libayatana-appindicator3-dev\
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-10-dev \ libclang-10-dev \

1
.gitignore vendored
View File

@@ -54,3 +54,4 @@ examples/**/target/
vcpkg_installed vcpkg_installed
flutter/lib/generated_plugin_registrant.dart flutter/lib/generated_plugin_registrant.dart
libsciter.dylib libsciter.dylib
flutter/web/

388
Cargo.lock generated
View File

@@ -123,7 +123,7 @@ checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android-wakelock" name = "android-wakelock"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/21pages/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90" source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90"
dependencies = [ dependencies = [
"jni 0.21.1", "jni 0.21.1",
"log", "log",
@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.4.0" version = "3.4.0"
source = "git+https://github.com/rustdesk-org/arboard#27b4e503caa70ec6306e5270461429f2cf907ad6" source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
dependencies = [ dependencies = [
"clipboard-win", "clipboard-win",
"core-graphics 0.23.2", "core-graphics 0.23.2",
@@ -234,24 +234,13 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation",
"parking_lot", "parking_lot",
"resvg", "serde 1.0.203",
"serde_derive",
"windows-sys 0.48.0", "windows-sys 0.48.0",
"wl-clipboard-rs", "wl-clipboard-rs",
"x11rb 0.13.1", "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]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.5.1" version = "0.5.1"
@@ -871,6 +860,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.38" version = "0.4.38"
@@ -987,11 +982,14 @@ dependencies = [
[[package]] [[package]]
name = "clipboard-master" name = "clipboard-master"
version = "4.0.0-beta.6" 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 = [ dependencies = [
"objc", "objc",
"objc-foundation", "objc-foundation",
"objc_id", "objc_id",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
"windows-win", "windows-win",
"wl-clipboard-rs", "wl-clipboard-rs",
"x11-clipboard 0.9.2", "x11-clipboard 0.9.2",
@@ -1000,9 +998,9 @@ dependencies = [
[[package]] [[package]]
name = "clipboard-win" name = "clipboard-win"
version = "5.3.1" version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
dependencies = [ dependencies = [
"error-code", "error-code",
] ]
@@ -1526,12 +1524,6 @@ dependencies = [
"dasp_sample", "dasp_sample",
] ]
[[package]]
name = "data-url"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]] [[package]]
name = "dbus" name = "dbus"
version = "0.9.7" version = "0.9.7"
@@ -1691,7 +1683,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [ dependencies = [
"libloading 0.7.4", "libloading 0.8.4",
] ]
[[package]] [[package]]
@@ -2081,12 +2073,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.0" version = "0.11.0"
@@ -2143,29 +2129,6 @@ dependencies = [
"libm", "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]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@@ -3087,8 +3050,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hwcodec" name = "hwcodec"
version = "0.6.0" version = "0.7.0"
source = "git+https://github.com/21pages/hwcodec#89879f2f02c6f74e88a4a43744a1153aec5b7e7f" source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26"
dependencies = [ dependencies = [
"bindgen 0.59.2", "bindgen 0.59.2",
"cc", "cc",
@@ -3213,16 +3176,10 @@ dependencies = [
"tiff", "tiff",
] ]
[[package]]
name = "imagesize"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
[[package]] [[package]]
name = "impersonate_system" name = "impersonate_system"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/21pages/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167" source = "git+https://github.com/rustdesk-org/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167"
dependencies = [ dependencies = [
"cc", "cc",
] ]
@@ -3462,16 +3419,6 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -3558,7 +3505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"windows-targets 0.48.5", "windows-targets 0.52.5",
] ]
[[package]] [[package]]
@@ -3733,7 +3680,7 @@ dependencies = [
[[package]] [[package]]
name = "machine-uid" name = "machine-uid"
version = "0.3.0" version = "0.3.0"
source = "git+https://github.com/21pages/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542" source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542"
dependencies = [ dependencies = [
"bindgen 0.59.2", "bindgen 0.59.2",
"cc", "cc",
@@ -3777,15 +3724,6 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.5" version = "0.6.5"
@@ -3847,14 +3785,6 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "muda" name = "muda"
version = "0.13.5" version = "0.13.5"
@@ -4043,11 +3973,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"cfg_aliases", "cfg_aliases 0.1.1",
"libc", "libc",
"memoffset 0.9.1", "memoffset 0.9.1",
] ]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"cfg_aliases 0.2.1",
"libc",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@@ -4732,15 +4674,9 @@ version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
dependencies = [ 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]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.5" version = "1.1.5"
@@ -5065,6 +5001,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quick-xml"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "0.6.13" version = "0.6.13"
@@ -5260,7 +5205,7 @@ dependencies = [
[[package]] [[package]]
name = "rdev" name = "rdev"
version = "0.5.0-2" version = "0.5.0-2"
source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488" source = "git+https://github.com/rustdesk-org/rdev#d4c1759926d693ba269e2cb8cf9f87b13e424e4e"
dependencies = [ dependencies = [
"cocoa 0.24.1", "cocoa 0.24.1",
"core-foundation 0.9.4", "core-foundation 0.9.4",
@@ -5414,31 +5359,6 @@ dependencies = [
"winreg 0.50.0", "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]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.8"
@@ -5463,18 +5383,6 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "rpassword" name = "rpassword"
version = "2.1.0" version = "2.1.0"
@@ -5572,7 +5480,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk" name = "rustdesk"
version = "1.2.7" version = "1.3.2"
dependencies = [ dependencies = [
"android-wakelock", "android-wakelock",
"android_logger", "android_logger",
@@ -5604,6 +5512,7 @@ dependencies = [
"flutter_rust_bridge", "flutter_rust_bridge",
"fon", "fon",
"fruitbasket", "fruitbasket",
"gtk",
"hbb_common", "hbb_common",
"hex", "hex",
"hound", "hound",
@@ -5618,7 +5527,7 @@ dependencies = [
"libpulse-simple-binding", "libpulse-simple-binding",
"mac_address", "mac_address",
"magnum-opus", "magnum-opus",
"mouce", "nix 0.29.0",
"num_cpus", "num_cpus",
"objc", "objc",
"objc_id", "objc_id",
@@ -5650,6 +5559,7 @@ dependencies = [
"system_shutdown", "system_shutdown",
"tao", "tao",
"tauri-winrt-notification", "tauri-winrt-notification",
"termios",
"totp-rs", "totp-rs",
"tray-icon", "tray-icon",
"url", "url",
@@ -5670,7 +5580,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.2.7" version = "1.3.2"
dependencies = [ dependencies = [
"brotli", "brotli",
"dirs 5.0.1", "dirs 5.0.1",
@@ -5853,22 +5763,6 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@@ -5905,7 +5799,7 @@ dependencies = [
[[package]] [[package]]
name = "sciter-rs" name = "sciter-rs"
version = "0.5.57" 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 = [ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
@@ -6159,27 +6053,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simplecss"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d"
dependencies = [
"log",
]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "0.2.3" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -6189,15 +6068,6 @@ dependencies = [
"autocfg 1.3.0", "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]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.13.2"
@@ -6268,15 +6138,6 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" 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]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@@ -6338,16 +6199,6 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 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]] [[package]]
name = "syn" name = "syn"
version = "0.15.44" version = "0.15.44"
@@ -6399,7 +6250,7 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.29.10" 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 = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -6593,11 +6444,11 @@ dependencies = [
[[package]] [[package]]
name = "tfc" name = "tfc"
version = "0.6.1" version = "0.7.0"
source = "git+https://github.com/rustdesk-org/The-Fat-Controller#9dd86151525fd010dc93f6bc9b6aedd1a75cc342" source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#de9c8ba480f166a9fc90aaa47bb0e84b443ea9c6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"core-graphics 0.22.3", "core-graphics 0.23.2",
"unicode-segmentation", "unicode-segmentation",
"winapi 0.3.9", "winapi 0.3.9",
"x11 2.19.0", "x11 2.19.0",
@@ -6687,32 +6538,6 @@ dependencies = [
"time-core", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.1" version = "1.6.1"
@@ -7004,12 +6829,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ttf-parser"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
@@ -7082,18 +6901,6 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.12"
@@ -7109,30 +6916,12 @@ dependencies = [
"tinyvec", "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]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.11.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-vo"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.13" version = "0.1.13"
@@ -7195,33 +6984,6 @@ dependencies = [
"log", "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]] [[package]]
name = "utf16string" name = "utf16string"
version = "0.2.0" version = "0.2.0"
@@ -7309,7 +7071,7 @@ dependencies = [
[[package]] [[package]]
name = "wallpaper" name = "wallpaper"
version = "3.2.0" version = "3.2.0"
source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf" source = "git+https://github.com/rustdesk-org/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf"
dependencies = [ dependencies = [
"dirs 5.0.1", "dirs 5.0.1",
"enquote", "enquote",
@@ -7414,9 +7176,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]] [[package]]
name = "wayland-backend" name = "wayland-backend"
version = "0.3.4" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993"
dependencies = [ dependencies = [
"cc", "cc",
"downcast-rs", "downcast-rs",
@@ -7428,9 +7190,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-client" name = "wayland-client"
version = "0.31.3" version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"rustix 0.38.34", "rustix 0.38.34",
@@ -7440,9 +7202,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols" name = "wayland-protocols"
version = "0.32.1" version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d0f1056570486e26a3773ec633885124d79ae03827de05ba6c85f79904026c" checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"wayland-backend", "wayland-backend",
@@ -7452,9 +7214,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols-wlr" name = "wayland-protocols-wlr"
version = "0.3.1" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7dab47671043d9f5397035975fe1cac639e5bca5cc0b3c32d09f01612e34d24" checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"wayland-backend", "wayland-backend",
@@ -7465,20 +7227,20 @@ dependencies = [
[[package]] [[package]]
name = "wayland-scanner" name = "wayland-scanner"
version = "0.31.2" version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6"
dependencies = [ dependencies = [
"proc-macro2 1.0.86", "proc-macro2 1.0.86",
"quick-xml 0.31.0", "quick-xml 0.34.0",
"quote 1.0.36", "quote 1.0.36",
] ]
[[package]] [[package]]
name = "wayland-sys" name = "wayland-sys"
version = "0.31.2" version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148"
dependencies = [ dependencies = [
"dlib", "dlib",
"log", "log",
@@ -7498,7 +7260,7 @@ dependencies = [
[[package]] [[package]]
name = "webm" name = "webm"
version = "1.1.0" version = "1.1.0"
source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
dependencies = [ dependencies = [
"webm-sys", "webm-sys",
] ]
@@ -7506,7 +7268,7 @@ dependencies = [
[[package]] [[package]]
name = "webm-sys" name = "webm-sys"
version = "1.0.4" version = "1.0.4"
source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64"
dependencies = [ dependencies = [
"cc", "cc",
] ]
@@ -8220,12 +7982,6 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "zbus" name = "zbus"
version = "3.15.2" version = "3.15.2"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.2.7" version = "1.3.2"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
@@ -90,7 +90,7 @@ enigo = { path = "libs/enigo", features = [ "with_serde" ] }
clipboard = { path = "libs/clipboard" } clipboard = { path = "libs/clipboard" }
ctrlc = "3.2" ctrlc = "3.2"
# arboard = { version = "3.4.0", features = ["wayland-data-control"] } # arboard = { version = "3.4.0", features = ["wayland-data-control"] }
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] } arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
system_shutdown = "4.0" system_shutdown = "4.0"
@@ -114,7 +114,7 @@ winapi = { version = "0.3", features = [
winreg = "0.11" winreg = "0.11"
windows-service = "0.6" windows-service = "0.6"
virtual_display = { path = "libs/virtual_display" } virtual_display = { path = "libs/virtual_display" }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" } impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
shared_memory = "0.12" shared_memory = "0.12"
tauri-winrt-notification = "0.1.2" tauri-winrt-notification = "0.1.2"
runas = "1.2" runas = "1.2"
@@ -138,7 +138,7 @@ image = "0.24"
keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
wallpaper = { git = "https://github.com/21pages/wallpaper.rs" } wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support # https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
@@ -152,7 +152,6 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
pulse = { package = "libpulse-binding", version = "2.27" } pulse = { package = "libpulse-binding", version = "2.27" }
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
async-process = "1.7" async-process = "1.7"
mouce = { git="https://github.com/rustdesk-org/mouce.git" }
evdev = { git="https://github.com/rustdesk-org/evdev" } evdev = { git="https://github.com/rustdesk-org/evdev" }
dbus = "0.9" dbus = "0.9"
dbus-crossroads = "0.5" 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} x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = {version = "2.3", optional = true} percent-encoding = {version = "2.3", optional = true}
once_cell = {version = "1.18", optional = true} once_cell = {version = "1.18", optional = true}
nix = { version = "0.29", features = ["term", "process"]}
gtk = "0.18"
termios = "0.3"
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13" android_logger = "0.13"
jni = "0.21" jni = "0.21"
android-wakelock = { git = "https://github.com/21pages/android-wakelock" } android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
[workspace] [workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br> <img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Servers</a> • <a href="#public-servers">Servers</a> •
<a href="#raw-steps-to-build">Build</a> • <a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> • <a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> • <a href="#file-structure">Structure</a> •
@@ -59,19 +59,19 @@ Please download Sciter dynamic library yourself.
```sh ```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
``` ```
### openSUSE Tumbleweed ### openSUSE Tumbleweed
```sh ```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
``` ```
### Fedora 28 (CentOS 8) ### Fedora 28 (CentOS 8)
```sh ```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
``` ```
### Arch (Manjaro) ### Arch (Manjaro)
@@ -171,3 +171,7 @@ Please ensure that you are running these commands from the root of the RustDesk
![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) ![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) ![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
## [Public Servers](#public-servers)
RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github)

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.7 version: 1.3.2
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.7 version: 1.3.2
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
@@ -37,6 +37,9 @@ AppDir:
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
universe multiverse universe multiverse
include: include:
# https://github.com/rustdesk/rustdesk/issues/9103
# Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try.
# We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it.
- libc6:amd64 - libc6:amd64
- libgtk-3-0 - libgtk-3-0
- libxcb-randr0 - libxcb-randr0

View File

@@ -31,6 +31,11 @@ def get_deb_arch() -> str:
return "amd64" return "amd64"
return custom_arch return custom_arch
def get_deb_extra_depends() -> str:
custom_arch = os.environ.get("DEB_ARCH")
if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so
return ", libatomic1"
return ""
def system2(cmd): def system2(cmd):
exit_code = os.system(cmd) exit_code = os.system(cmd)
@@ -278,14 +283,17 @@ def generate_control_file(version):
system2('/bin/rm -rf %s' % control_file_path) system2('/bin/rm -rf %s' % control_file_path)
content = """Package: rustdesk content = """Package: rustdesk
Section: net
Priority: optional
Version: %s Version: %s
Architecture: %s Architecture: %s
Maintainer: rustdesk <info@rustdesk.com> Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com Homepage: https://rustdesk.com
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software. Description: A remote control software.
""" % (version, get_deb_arch()) """ % (version, get_deb_arch(), get_deb_extra_depends())
file = open(control_file_path, "w") file = open(control_file_path, "w")
file.write(content) file.write(content)
file.close() file.close()
@@ -325,8 +333,6 @@ def build_flutter_deb(version, features):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2( system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2( system2(
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
system2( system2(
@@ -370,8 +376,6 @@ def build_deb_from_folder(version, binary_folder):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2( system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
system2( system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")

View 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
View 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) 进行交流。

View File

@@ -1,20 +1,18 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br> <img src="../res/logo-header.svg" alt="RustDesk - Ваша віддалена стільниця"><br>
<a href="#безкоштовні-загальнодоступні-сервери">Сервери</a> • <a href="#публічні-сервери">Сервери</a> •
<a href="#кроки-для-збірки">Збирання</a> • <a href="#кроки-для-збірки">Збирання</a> •
<a href="#як-зібрати-за-допомогою-docker">Docker</a> • <a href="#як-зібрати-за-допомогою-docker">Docker</a> •
<a href="#структура-файлів">Структура</a> • <a href="#структура-файлів">Структура</a> •
<a href="#знімки">Знімки</a><br> <a href="#знімки-екрана">Знімки екрана</a><br>
[<a href="../README.md">English</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br> [<a href="../README.md">English</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>]<br>
<b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk на вашу рідну мову</B> <b>Нам потрібна ваша допомога для перекладу цього README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">інтерфейсу</a> та <a href="https://github.com/rustdesk/doc.rustdesk.com">документації</a> RustDesk вашою рідною мовою</B>
</p> </p>
Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open)
Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo). Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
@@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT
```sh ```sh
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
``` ```
### openSUSE Tumbleweed ### openSUSE Tumbleweed
```sh ```sh
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
``` ```
### Fedora 28 (CentOS 8) ### Fedora 28 (CentOS 8)
```sh ```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
``` ```
### Arch (Manjaro) ### Arch (Manjaro)
@@ -158,18 +156,22 @@ target/release/rustdesk
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS. - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове зʼєднання
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter
## Знімки ## Знімки екрана
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) ![Менеджер зʼєднань](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) ![Підключення до ПК з Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![Передача файлів](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) ![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
## [Публічні сервери](#публічні-сервери)
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)

View File

@@ -8,7 +8,7 @@
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br> [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
</p> </p>
Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) 与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
@@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md). RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md).
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
@@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m
## 依赖 ## 依赖
桌面版本界面使用[sciter](https://sciter.com/), 请自行下载 桌面版本使用 Flutter 或 Sciter已弃用作为 GUI本教程仅适用于 Sciter因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本
请自行下载Sciter动态库。
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
@@ -207,12 +209,13 @@ target/release/rustdesk
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI已弃用
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现 - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继) - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码 - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
## 截图 ## 截图

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="-4 -4 32 32" width="24px" fill="#5f6368"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -43,15 +43,13 @@ arm64-v8a)
FLUTTER_TARGET=android-arm64 FLUTTER_TARGET=android-arm64
NDK_TARGET=aarch64-linux-android NDK_TARGET=aarch64-linux-android
RUST_TARGET=aarch64-linux-android RUST_TARGET=aarch64-linux-android
# RUSTDESK_FEATURES='flutter,hwcodec' RUSTDESK_FEATURES='flutter,hwcodec'
RUSTDESK_FEATURES='flutter'
;; ;;
armeabi-v7a) armeabi-v7a)
FLUTTER_TARGET=android-arm FLUTTER_TARGET=android-arm
NDK_TARGET=arm-linux-androideabi NDK_TARGET=arm-linux-androideabi
RUST_TARGET=armv7-linux-androideabi RUST_TARGET=armv7-linux-androideabi
# RUSTDESK_FEATURES='flutter,hwcodec' RUSTDESK_FEATURES='flutter,hwcodec'
RUSTDESK_FEATURES='flutter'
;; ;;
x86_64) x86_64)
FLUTTER_TARGET=android-x64 FLUTTER_TARGET=android-x64
@@ -304,6 +302,7 @@ prebuild)
sed \ sed \
-i \ -i \
-e 's/extended_text: .*/extended_text: 11.1.0/' \
-e 's/uni_links_desktop/#uni_links_desktop/g' \ -e 's/uni_links_desktop/#uni_links_desktop/g' \
flutter/pubspec.yaml flutter/pubspec.yaml

View File

@@ -30,8 +30,8 @@ import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart'; import 'mobile/pages/remote_page.dart';
import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/remote_page.dart' as desktop_remote;
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'models/input_model.dart';
import 'models/model.dart'; import 'models/model.dart';
import 'models/platform_model.dart'; import 'models/platform_model.dart';
@@ -51,6 +51,9 @@ final isLinux = isLinux_;
final isDesktop = isDesktop_; final isDesktop = isDesktop_;
final isWeb = isWeb_; final isWeb = isWeb_;
final isWebDesktop = isWebDesktop_; final isWebDesktop = isWebDesktop_;
final isWebOnWindows = isWebOnWindows_;
final isWebOnLinux = isWebOnLinux_;
final isWebOnMacOs = isWebOnMacOS_;
var isMobile = isAndroid || isIOS; var isMobile = isAndroid || isIOS;
var version = ''; var version = '';
int androidVersion = 0; int androidVersion = 0;
@@ -348,6 +351,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 224, 224, 224), hoverColor: Color.fromARGB(255, 224, 224, 224),
scaffoldBackgroundColor: Colors.white, scaffoldBackgroundColor: Colors.white,
dialogBackgroundColor: Colors.white, dialogBackgroundColor: Colors.white,
appBarTheme: AppBarTheme(
shadowColor: Colors.transparent,
),
dialogTheme: DialogTheme( dialogTheme: DialogTheme(
elevation: 15, elevation: 15,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -443,6 +449,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 45, 46, 53), hoverColor: Color.fromARGB(255, 45, 46, 53),
scaffoldBackgroundColor: Color(0xFF18191E), scaffoldBackgroundColor: Color(0xFF18191E),
dialogBackgroundColor: Color(0xFF18191E), dialogBackgroundColor: Color(0xFF18191E),
appBarTheme: AppBarTheme(
shadowColor: Colors.transparent,
),
dialogTheme: DialogTheme( dialogTheme: DialogTheme(
elevation: 15, elevation: 15,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -546,9 +555,9 @@ class MyTheme {
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
} }
static void changeDarkMode(ThemeMode mode) async { static Future<void> changeDarkMode(ThemeMode mode) async {
Get.changeThemeMode(mode); Get.changeThemeMode(mode);
if (desktopType == DesktopType.main || isAndroid || isIOS) { if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
if (mode == ThemeMode.system) { if (mode == ThemeMode.system) {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: defaultOptionTheme); key: kCommConfKeyTheme, value: defaultOptionTheme);
@@ -556,7 +565,7 @@ class MyTheme {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString()); key: kCommConfKeyTheme, value: mode.toShortString());
} }
await bind.mainChangeTheme(dark: mode.toShortString()); if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
// Synchronize the window theme of the system. // Synchronize the window theme of the system.
updateSystemWindowTheme(); updateSystemWindowTheme();
} }
@@ -630,10 +639,30 @@ List<Locale> supportedLocales = const [
Locale('da'), Locale('da'),
Locale('eo'), Locale('eo'),
Locale('tr'), Locale('tr'),
Locale('vi'),
Locale('pl'),
Locale('kz'), Locale('kz'),
Locale('es'), Locale('es'),
Locale('nl'),
Locale('nb'),
Locale('et'),
Locale('eu'),
Locale('bg'),
Locale('be'),
Locale('vn'),
Locale('uk'),
Locale('fa'),
Locale('ca'),
Locale('el'),
Locale('sv'),
Locale('sq'),
Locale('sr'),
Locale('th'),
Locale('sl'),
Locale('ro'),
Locale('lt'),
Locale('lv'),
Locale('ar'),
Locale('he'),
Locale('hr'),
]; ];
String formatDurationToTime(Duration duration) { String formatDurationToTime(Duration duration) {
@@ -647,11 +676,17 @@ String formatDurationToTime(Duration duration) {
closeConnection({String? id}) { closeConnection({String? id}) {
if (isAndroid || isIOS) { if (isAndroid || isIOS) {
gFFI.chatModel.hideChatOverlay(); () async {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
gFFI.chatModel.hideChatOverlay();
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
stateGlobal.isInMainPage = true;
}();
} else { } else {
if (isWeb) { if (isWeb) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
stateGlobal.isInMainPage = true;
} else { } else {
final controller = Get.find<DesktopTabController>(); final controller = Get.find<DesktopTabController>();
controller.closeBy(id); controller.closeBy(id);
@@ -1054,6 +1089,49 @@ class CustomAlertDialog extends StatelessWidget {
} }
} }
Widget createDialogContent(String text) {
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
final List<TextSpan> spans = [];
int start = 0;
bool hasLink = false;
linkRegExp.allMatches(text).forEach((match) {
hasLink = true;
if (match.start > start) {
spans.add(TextSpan(text: text.substring(start, match.start)));
}
spans.add(TextSpan(
text: match.group(0) ?? '',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String linkText = match.group(0) ?? '';
linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
launchUrl(Uri.parse(linkText));
},
));
start = match.end;
});
if (start < text.length) {
spans.add(TextSpan(text: text.substring(start)));
}
if (!hasLink) {
return SelectableText(text, style: const TextStyle(fontSize: 15));
}
return SelectableText.rich(
TextSpan(
style: TextStyle(color: Colors.black, fontSize: 15),
children: spans,
),
);
}
void msgBox(SessionID sessionId, String type, String title, String text, void msgBox(SessionID sessionId, String type, String title, String text,
String link, OverlayDialogManager dialogManager, String link, OverlayDialogManager dialogManager,
{bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) { {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
@@ -1211,7 +1289,7 @@ Widget msgboxContent(String type, String title, String text) {
translate(title), translate(title),
style: TextStyle(fontSize: 21), style: TextStyle(fontSize: 21),
).marginOnly(bottom: 10), ).marginOnly(bottom: 10),
Text(translateText(text), style: const TextStyle(fontSize: 15)), createDialogContent(translateText(text)),
], ],
), ),
), ),
@@ -1406,14 +1484,10 @@ class AndroidPermissionManager {
} }
} }
// TODO move this to mobile/widgets.
// Used only for mobile, pages remote, settings, dialog
// TODO remove argument contentPadding, its not used, getToggle() has not
RadioListTile<T> getRadio<T>( RadioListTile<T> getRadio<T>(
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange, Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
{EdgeInsetsGeometry? contentPadding, bool? dense}) { {bool? dense}) {
return RadioListTile<T>( return RadioListTile<T>(
contentPadding: contentPadding ?? EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
controlAffinity: ListTileControlAffinity.trailing, controlAffinity: ListTileControlAffinity.trailing,
title: title, title: title,
@@ -1964,6 +2038,8 @@ Future<bool> restoreWindowPosition(WindowType type,
return false; return false;
} }
var webInitialLink = "";
/// Initialize uni links for macos/windows /// Initialize uni links for macos/windows
/// ///
/// [Availability] /// [Availability]
@@ -1980,7 +2056,12 @@ Future<bool> initUniLinks() async {
if (initialLink == null || initialLink.isEmpty) { if (initialLink == null || initialLink.isEmpty) {
return false; return false;
} }
return handleUriLink(uriString: initialLink); if (isWeb) {
webInitialLink = initialLink;
return false;
} else {
return handleUriLink(uriString: initialLink);
}
} catch (err) { } catch (err) {
debugPrintStack(label: "$err"); debugPrintStack(label: "$err");
return false; return false;
@@ -1993,7 +2074,7 @@ Future<bool> initUniLinks() async {
/// ///
/// Returns a [StreamSubscription] which can listen the uni links. /// Returns a [StreamSubscription] which can listen the uni links.
StreamSubscription? listenUniLinks({handleByFlutter = true}) { StreamSubscription? listenUniLinks({handleByFlutter = true}) {
if (isLinux) { if (isLinux || isWeb) {
return null; return null;
} }
@@ -2297,20 +2378,35 @@ connect(BuildContext context, String id,
} }
} else { } else {
if (isFileTransfer) { if (isFileTransfer) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) { if (isAndroid) {
if (!await AndroidPermissionManager.request(kManageExternalStorage)) { if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
return; if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
return;
}
} }
} }
Navigator.push( if (isWeb) {
context, Navigator.push(
MaterialPageRoute( context,
builder: (BuildContext context) => FileManagerPage( MaterialPageRoute(
id: id, password: password, isSharedPassword: isSharedPassword), builder: (BuildContext context) =>
), desktop_file_manager.FileManagerPage(
); id: id,
password: password,
isSharedPassword: isSharedPassword),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(
id: id, password: password, isSharedPassword: isSharedPassword),
),
);
}
} else { } else {
if (isWebDesktop) { if (isWeb) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -2334,6 +2430,7 @@ connect(BuildContext context, String id,
); );
} }
} }
stateGlobal.isInMainPage = false;
} }
FocusScopeNode currentFocus = FocusScope.of(context); FocusScopeNode currentFocus = FocusScope.of(context);
@@ -2814,7 +2911,7 @@ Widget buildErrorBanner(BuildContext context,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Tooltip( child: Tooltip(
message: translate(err.value), message: translate(err.value),
child: Text( child: SelectableText(
translate(err.value), translate(err.value),
), ),
)).marginSymmetric(vertical: 2), )).marginSymmetric(vertical: 2),
@@ -2935,6 +3032,16 @@ openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
final displays = i == kAllDisplayValue final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index) ? List.generate(pi.displays.length, (index) => index)
: [i]; : [i];
// Try clear image model before switching from all displays
// 1. The remote side has multiple displays.
// 2. Do not use texture render.
// 3. Connect to Display 1.
// 4. Switch to multi-displays `kAllDisplayValue`
// 5. Switch to Display 2.
// Then the remote page will display last picture of Display 1 at the beginning.
if (pi.forceTextureRender && i != kAllDisplayValue) {
ffi.imageModel.clearImage();
}
bind.sessionSwitchDisplay( bind.sessionSwitchDisplay(
isDesktop: isDesktop, isDesktop: isDesktop,
sessionId: ffi.sessionId, sessionId: ffi.sessionId,
@@ -2968,11 +3075,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args)); kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
} }
setNewConnectWindowFrame( setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
int windowId, String peerId, int? display, Rect? screenRect) async { int? display, Rect? screenRect) async {
if (screenRect == null) { if (screenRect == null) {
await restoreWindowPosition(WindowType.RemoteDesktop, // Do not restore window position to new connection if there's a pre-session.
windowId: windowId, display: display, peerId: peerId); // https://github.com/rustdesk/rustdesk/discussions/8825
if (preSessionCount == 0) {
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: windowId, display: display, peerId: peerId);
}
} else { } else {
await tryMoveToScreenAndSetFullscreen(screenRect); await tryMoveToScreenAndSetFullscreen(screenRect);
} }
@@ -3069,9 +3180,13 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs, importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
String? text) { String? text) {
text = text?.trim();
if (text != null && text.isNotEmpty) { if (text != null && text.isNotEmpty) {
try { try {
final sc = ServerConfig.decode(text); final sc = ServerConfig.decode(text);
if (isWeb || isIOS) {
sc.relayServer = '';
}
if (sc.idServer.isNotEmpty) { if (sc.idServer.isNotEmpty) {
Future<bool> success = setServerConfig(controllers, errMsgs, sc); Future<bool> success = setServerConfig(controllers, errMsgs, sc);
success.then((value) { success.then((value) {
@@ -3316,6 +3431,42 @@ bool isInHomePage() {
return controller.state.value.selected == 0; 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() { Widget buildPresetPasswordWarning() {
return FutureBuilder<bool>( return FutureBuilder<bool>(
future: bind.isPresetPassword(), future: bind.isPresetPassword(),
@@ -3326,32 +3477,7 @@ Widget buildPresetPasswordWarning() {
return Text( return Text(
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error 'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
} else if (snapshot.hasData && snapshot.data == true) { } else if (snapshot.hasData && snapshot.data == true) {
if (bind.mainGetBuildinOption( return _buildPresetPasswordWarning();
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
} else { } else {
return SizedBox return SizedBox
.shrink(); // Show nothing if the Future completed with false or null .shrink(); // Show nothing if the Future completed with false or null
@@ -3405,7 +3531,8 @@ Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
); );
} }
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize; get windowResizeEdgeSize =>
isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable. // `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
// See _linuxWindowResizable for more details. // See _linuxWindowResizable for more details.
@@ -3423,7 +3550,12 @@ setResizable(bool resizable) {
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key); isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
final isCustomClient = bind.isCustomClient(); bool? _isCustomClient;
bool get isCustomClient {
_isCustomClient ??= bind.isCustomClient();
return _isCustomClient!;
}
get defaultOptionLang => isCustomClient ? 'default' : ''; get defaultOptionLang => isCustomClient ? 'default' : '';
get defaultOptionTheme => isCustomClient ? 'system' : ''; get defaultOptionTheme => isCustomClient ? 'system' : '';
get defaultOptionYes => isCustomClient ? 'Y' : ''; get defaultOptionYes => isCustomClient ? 'Y' : '';
@@ -3461,3 +3593,40 @@ disableWindowMovable(int? windowId) {
WindowController.fromWindowId(windowId).setMovable(false); 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;
void earlyAssert() {
assert('\1' == '1');
}

View File

@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -35,17 +36,14 @@ class AddressBook extends StatefulWidget {
class _AddressBookState extends State<AddressBook> { class _AddressBookState extends State<AddressBook> {
var menuPos = RelativeRect.fill; var menuPos = RelativeRect.fill;
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) => Obx(() { Widget build(BuildContext context) => Obx(() {
if (!gFFI.userModel.isLogin) { if (!gFFI.userModel.isLogin) {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} else if (gFFI.userModel.networkError.isNotEmpty) {
return netWorkErrorWidget();
} else { } else {
return Column( return Column(
children: [ children: [
@@ -64,15 +62,16 @@ class _AddressBookState extends State<AddressBook> {
retry: null, // remove retry retry: null, // remove retry
close: () => gFFI.abModel.currentAbPushError.value = ''), close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded( Expanded(
child: (isDesktop || isWebDesktop) child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildAddressBookDesktop() ? _buildAddressBookPortrait()
: _buildAddressBookMobile()) : _buildAddressBookLandscape()),
),
], ],
); );
} }
}); });
Widget _buildAddressBookDesktop() { Widget _buildAddressBookLandscape() {
return Row( return Row(
children: [ children: [
Offstage( Offstage(
@@ -109,7 +108,7 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
Widget _buildAddressBookMobile() { Widget _buildAddressBookPortrait() {
const padding = 8.0; const padding = 8.0;
return Column( return Column(
children: [ children: [
@@ -242,14 +241,15 @@ class _AddressBookState extends State<AddressBook> {
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value); bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
} }
}, },
customButton: Container( customButton: Obx(() => Container(
height: isDesktop ? 48 : 40, height: stateGlobal.isPortrait.isFalse ? 48 : 40,
child: Row(children: [ child: Row(children: [
Expanded( Expanded(
child: buildItem(gFFI.abModel.currentName.value, button: true)), child:
Icon(Icons.arrow_drop_down), buildItem(gFFI.abModel.currentName.value, button: true)),
]), Icon(Icons.arrow_drop_down),
), ]),
)),
underline: Container( underline: Container(
height: 0.7, height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1), color: Theme.of(context).dividerColor.withOpacity(0.1),
@@ -338,8 +338,8 @@ class _AddressBookState extends State<AddressBook> {
showActionMenu: editPermission); showActionMenu: editPermission);
} }
final gridView = DynamicGridView.builder( gridView(bool isPortrait) => DynamicGridView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
gridDelegate: SliverGridDelegateWithWrapping(), gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length, itemCount: tags.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -347,9 +347,9 @@ class _AddressBookState extends State<AddressBook> {
return tagBuilder(e); return tagBuilder(e);
}); });
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop) return Obx(() => stateGlobal.isPortrait.isFalse
? gridView ? gridView(false)
: LimitedBox(maxHeight: maxHeight, child: gridView); : LimitedBox(maxHeight: maxHeight, child: gridView(true)));
}); });
} }
@@ -359,7 +359,6 @@ class _AddressBookState extends State<AddressBook> {
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: AddressBookPeersView( child: AddressBookPeersView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.abModel.currentAbPeers,
)), )),
); );
} }
@@ -425,7 +424,8 @@ class _AddressBookState extends State<AddressBook> {
if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb), if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb),
if (canWrite) getEntry(translate("Add Tag"), abAddTag), if (canWrite) getEntry(translate("Add Tag"), abAddTag),
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(), if (gFFI.abModel.legacyMode.value)
sortMenuItem(), // It's already sorted after pulling down
if (canWrite) syncMenuItem(), if (canWrite) syncMenuItem(),
filterMenuItem(), filterMenuItem(),
if (!gFFI.abModel.legacyMode.value && canWrite) if (!gFFI.abModel.legacyMode.value && canWrite)
@@ -508,20 +508,21 @@ class _AddressBookState extends State<AddressBook> {
double marginBottom = 4; double marginBottom = 4;
row({required Widget lable, required Widget input}) { row({required Widget lable, required Widget input}) {
return Row( makeChild(bool isPortrait) => Row(
children: [ children: [
!isMobile !isPortrait
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: lable.marginOnly(right: 10)) child: lable.marginOnly(right: 10))
: SizedBox.shrink(), : SizedBox.shrink(),
Expanded( Expanded(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 200), constraints: const BoxConstraints(minWidth: 200),
child: input), child: input),
), ),
], ],
).marginOnly(bottom: !isMobile ? 8 : 0); ).marginOnly(bottom: !isPortrait ? 8 : 0);
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
} }
return CustomAlertDialog( return CustomAlertDialog(
@@ -544,23 +545,28 @@ class _AddressBookState extends State<AddressBook> {
), ),
], ],
), ),
input: TextField( input: Obx(() => TextField(
controller: idController, controller: idController,
inputFormatters: [IDTextInputFormatter()], inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('ID'), labelText: stateGlobal.isPortrait.isFalse
errorText: errorMsg, ? null
errorMaxLines: 5), : translate('ID'),
)), errorText: errorMsg,
errorMaxLines: 5),
))),
row( row(
lable: Text( lable: Text(
translate('Alias'), translate('Alias'),
style: style, style: style,
), ),
input: TextField( input: Obx(() => TextField(
controller: aliasController, controller: aliasController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('Alias'), labelText: stateGlobal.isPortrait.isFalse
? null
: translate('Alias'),
),
)), )),
), ),
if (isCurrentAbShared) if (isCurrentAbShared)
@@ -569,22 +575,26 @@ class _AddressBookState extends State<AddressBook> {
translate('Password'), translate('Password'),
style: style, style: style,
), ),
input: TextField( input: Obx(
controller: passwordController, () => TextField(
obscureText: !passwordVisible, controller: passwordController,
decoration: InputDecoration( obscureText: !passwordVisible,
labelText: !isMobile ? null : translate('Password'), decoration: InputDecoration(
suffixIcon: IconButton( labelText: stateGlobal.isPortrait.isFalse
icon: Icon( ? null
passwordVisible : translate('Password'),
? Icons.visibility suffixIcon: IconButton(
: Icons.visibility_off, icon: Icon(
color: MyTheme.lightTheme.primaryColor), passwordVisible
onPressed: () { ? Icons.visibility
setState(() { : Icons.visibility_off,
passwordVisible = !passwordVisible; color: MyTheme.lightTheme.primaryColor),
}); onPressed: () {
}, setState(() {
passwordVisible = !passwordVisible;
});
},
),
), ),
), ),
)), )),

View File

@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
.map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: widget.peer.tags.isNotEmpty : widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'

View File

@@ -0,0 +1,38 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common.dart';
Widget getConnectionPageTitle(BuildContext context, bool isWeb) {
return Row(
children: [
Expanded(
child: Row(
children: [
AutoSizeText(
translate('Control Remote Desktop'),
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleLarge
?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)),
],
);
}

View File

@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
String get name => translate('uppercase'); String get name => translate('uppercase');
@override @override
bool validate(String value) { bool validate(String value) {
return value.contains(RegExp(r'[A-Z]')); return value.runes.any((int rune) {
var character = String.fromCharCode(rune);
return character.toUpperCase() == character &&
character.toLowerCase() != character;
});
} }
} }
@@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule {
@override @override
bool validate(String value) { bool validate(String value) {
return value.contains(RegExp(r'[a-z]')); return value.runes.any((int rune) {
var character = String.fromCharCode(rune);
return character.toLowerCase() == character &&
character.toUpperCase() != character;
});
} }
} }

View File

@@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
@@ -380,6 +381,7 @@ class DialogTextField extends StatelessWidget {
final FocusNode? focusNode; final FocusNode? focusNode;
final TextInputType? keyboardType; final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters; final List<TextInputFormatter>? inputFormatters;
final int? maxLength;
static const kUsernameTitle = 'Username'; static const kUsernameTitle = 'Username';
static const kUsernameIcon = Icon(Icons.account_circle_outlined); static const kUsernameIcon = Icon(Icons.account_circle_outlined);
@@ -397,6 +399,7 @@ class DialogTextField extends StatelessWidget {
this.hintText, this.hintText,
this.keyboardType, this.keyboardType,
this.inputFormatters, this.inputFormatters,
this.maxLength,
required this.title, required this.title,
required this.controller}) required this.controller})
: super(key: key); : super(key: key);
@@ -423,6 +426,7 @@ class DialogTextField extends StatelessWidget {
obscureText: obscureText, obscureText: obscureText,
keyboardType: keyboardType, keyboardType: keyboardType,
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
maxLength: maxLength,
), ),
), ),
], ],
@@ -679,6 +683,8 @@ class PasswordWidget extends StatefulWidget {
this.reRequestFocus = false, this.reRequestFocus = false,
this.hintText, this.hintText,
this.errorText, this.errorText,
this.title,
this.maxLength,
}) : super(key: key); }) : super(key: key);
final TextEditingController controller; final TextEditingController controller;
@@ -686,6 +692,8 @@ class PasswordWidget extends StatefulWidget {
final bool reRequestFocus; final bool reRequestFocus;
final String? hintText; final String? hintText;
final String? errorText; final String? errorText;
final String? title;
final int? maxLength;
@override @override
State<PasswordWidget> createState() => _PasswordWidgetState(); State<PasswordWidget> createState() => _PasswordWidgetState();
@@ -729,7 +737,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DialogTextField( return DialogTextField(
title: translate(DialogTextField.kPasswordTitle), title: translate(widget.title ?? DialogTextField.kPasswordTitle),
hintText: translate(widget.hintText ?? 'Enter your password'), hintText: translate(widget.hintText ?? 'Enter your password'),
controller: widget.controller, controller: widget.controller,
prefixIcon: DialogTextField.kPasswordIcon, prefixIcon: DialogTextField.kPasswordIcon,
@@ -748,6 +756,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
obscureText: !_passwordVisible, obscureText: !_passwordVisible,
errorText: widget.errorText, errorText: widget.errorText,
focusNode: _focusNode, focusNode: _focusNode,
maxLength: widget.maxLength,
); );
} }
} }
@@ -1121,7 +1130,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value, errorText: errPwd.isEmpty ? null : errPwd.value,
), ),
], ],
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0), ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
).marginOnly(top: 10), ).marginOnly(top: 10),
], ],
), ),
@@ -1829,6 +1838,7 @@ void changeBot({Function()? callback}) async {
void change2fa({Function()? callback}) async { void change2fa({Function()? callback}) async {
if (bind.mainHasValid2FaSync()) { if (bind.mainHasValid2FaSync()) {
await bind.mainSetOption(key: "2fa", value: ""); await bind.mainSetOption(key: "2fa", value: "");
await bind.mainClearTrustedDevices();
callback?.call(); callback?.call();
return; return;
} }
@@ -1896,6 +1906,7 @@ void enter2FaDialog(
SessionID sessionId, OverlayDialogManager dialogManager) async { SessionID sessionId, OverlayDialogManager dialogManager) async {
final controller = TextEditingController(); final controller = TextEditingController();
final RxBool submitReady = false.obs; final RxBool submitReady = false.obs;
final RxBool trustThisDevice = false.obs;
dialogManager.dismissAll(); dialogManager.dismissAll();
dialogManager.show((setState, close, context) { dialogManager.show((setState, close, context) {
@@ -1905,7 +1916,7 @@ void enter2FaDialog(
} }
submit() { submit() {
gFFI.send2FA(sessionId, controller.text.trim()); gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
close(); close();
dialogManager.showLoading(translate('Logging in...'), dialogManager.showLoading(translate('Logging in...'),
onCancel: closeConnection); onCancel: closeConnection);
@@ -1919,9 +1930,27 @@ void enter2FaDialog(
onChanged: () => submitReady.value = codeField.isReady, onChanged: () => submitReady.value = codeField.isReady,
); );
final trustField = Obx(() => CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(translate("Trust this device")),
value: trustThisDevice.value,
onChanged: (value) {
if (value == null) return;
trustThisDevice.value = value;
},
));
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate('enter-2fa-title')), title: Text(translate('enter-2fa-title')),
content: codeField, content: Column(
children: [
codeField,
if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
trustField,
],
),
actions: [ actions: [
dialogButton('Cancel', dialogButton('Cancel',
onPressed: cancel, onPressed: cancel,
@@ -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(),
)),
);
}

View File

@@ -142,11 +142,6 @@ class _WidgetOPState extends State<WidgetOP> {
String _failedMsg = ''; String _failedMsg = '';
String _url = ''; String _url = '';
@override
void initState() {
super.initState();
}
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/login.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
@@ -23,11 +24,6 @@ class _MyGroupState extends State<MyGroup> {
RxString get searchUserText => gFFI.groupModel.searchUserText; RxString get searchUserText => gFFI.groupModel.searchUserText;
static TextEditingController searchUserController = TextEditingController(); static TextEditingController searchUserController = TextEditingController();
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
@@ -35,6 +31,8 @@ class _MyGroupState extends State<MyGroup> {
return Center( return Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login")))); onPressed: loginDialog, child: Text(translate("Login"))));
} else if (gFFI.userModel.networkError.isNotEmpty) {
return netWorkErrorWidget();
} else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) { } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
@@ -48,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
retry: null, retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''), close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded( Expanded(
child: (isDesktop || isWebDesktop) child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildDesktop() ? _buildPortrait()
: _buildMobile()) : _buildLandscape())),
], ],
); );
}); });
} }
Widget _buildDesktop() { Widget _buildLandscape() {
return Row( return Row(
children: [ children: [
Container( Container(
@@ -85,14 +83,14 @@ class _MyGroupState extends State<MyGroup> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.groupModel.peers)), )),
) )
], ],
); );
} }
Widget _buildMobile() { Widget _buildPortrait() {
return Column( return Column(
children: [ children: [
Container( Container(
@@ -117,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.groupModel.peers)), )),
) )
], ],
); );
@@ -162,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
} }
return true; return true;
}).toList(); }).toList();
final listView = ListView.builder( listView(bool isPortrait) => ListView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index])); itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop) return Obx(() => stateGlobal.isPortrait.isFalse
? listView ? listView(false)
: LimitedBox(maxHeight: maxHeight, child: listView); : LimitedBox(maxHeight: maxHeight, child: listView(true)));
}); });
} }

View File

@@ -353,7 +353,7 @@ class Draggable extends StatefulWidget {
final Widget Function(BuildContext, GestureDragUpdateCallback) builder; final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
@override @override
State<StatefulWidget> createState() => _DraggableState(); State<StatefulWidget> createState() => _DraggableState(chatModel);
} }
class _DraggableState extends State<Draggable> { class _DraggableState extends State<Draggable> {
@@ -362,10 +362,8 @@ class _DraggableState extends State<Draggable> {
double _saveHeight = 0; double _saveHeight = 0;
double _lastBottomHeight = 0; double _lastBottomHeight = 0;
@override _DraggableState(ChatModel? chatModel) {
void initState() { _chatModel = chatModel;
super.initState();
_chatModel = widget.chatModel;
} }
get position => widget.position.pos; get position => widget.position.pos;
@@ -467,7 +465,8 @@ class IOSDraggable extends StatefulWidget {
final Widget Function(BuildContext) builder; final Widget Function(BuildContext) builder;
@override @override
IOSDraggableState createState() => IOSDraggableState(); IOSDraggableState createState() =>
IOSDraggableState(chatModel, width, height);
} }
class IOSDraggableState extends State<IOSDraggable> { class IOSDraggableState extends State<IOSDraggable> {
@@ -478,12 +477,10 @@ class IOSDraggableState extends State<IOSDraggable> {
double _saveHeight = 0; double _saveHeight = 0;
double _lastBottomHeight = 0; double _lastBottomHeight = 0;
@override IOSDraggableState(ChatModel? chatModel, double w, double h) {
void initState() { _chatModel = chatModel;
super.initState(); _width = w;
_chatModel = widget.chatModel; _height = h;
_width = widget.width;
_height = widget.height;
} }
DraggableKeyPosition get position => widget.position; DraggableKeyPosition get position => widget.position;

View File

@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -53,42 +54,44 @@ class _PeerCardState extends State<_PeerCard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
if (isDesktop || isWebDesktop) { return Obx(() =>
return _buildDesktop(); stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
} else {
return _buildMobile();
}
} }
Widget _buildMobile() { Widget gestureDetector({required Widget child}) {
final peer = super.widget.peer;
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer;
return GestureDetector(
onDoubleTap: peerTabModel.multiSelectionMode
? null
: () => widget.connect(context, peer.id),
onTap: () {
if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (isMobile) {
widget.connect(context, peer.id);
} else {
peerTabModel.select(peer);
}
}
},
onLongPress: () => peerTabModel.select(peer),
child: child);
}
Widget _buildPortrait() {
final peer = super.widget.peer;
return Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 2), margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector( child: gestureDetector(
onTap: () {
if (peerTabModel.multiSelectionMode) {
peerTabModel.select(peer);
} else {
if (!isWebDesktop) {
connectInPeerTab(context, peer, widget.tab);
}
}
},
onDoubleTap: isWebDesktop
? () => connectInPeerTab(context, peer, widget.tab)
: null,
onLongPress: () {
peerTabModel.select(peer);
},
child: Container( child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
child: _buildPeerTile(context, peer, null)), child: _buildPeerTile(context, peer, null)),
)); ));
} }
Widget _buildDesktop() { Widget _buildLandscape() {
final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer; final peer = super.widget.peer;
var deco = Rx<BoxDecoration?>( var deco = Rx<BoxDecoration?>(
BoxDecoration( BoxDecoration(
@@ -117,36 +120,27 @@ class _PeerCardState extends State<_PeerCard>
), ),
); );
}, },
child: GestureDetector( child: gestureDetector(
onDoubleTap:
peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
? null
: () => widget.connect(context, peer.id),
onTap: () => peerTabModel.select(peer),
onLongPress: () => peerTabModel.select(peer),
child: Obx(() => peerCardUiType.value == PeerUiType.grid child: Obx(() => peerCardUiType.value == PeerUiType.grid
? _buildPeerCard(context, peer, deco) ? _buildPeerCard(context, peer, deco)
: _buildPeerTile(context, peer, deco))), : _buildPeerTile(context, peer, deco))),
); );
} }
Widget _buildPeerTile( makeChild(bool isPortrait, Peer peer) {
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final name = hideUsernameOnCard == true final name = hideUsernameOnCard == true
? peer.hostname ? peer.hostname
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Row( return Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f), color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile borderRadius: isPortrait
? BorderRadius.circular(_tileRadius) ? BorderRadius.circular(_tileRadius)
: BorderRadius.only( : BorderRadius.only(
topLeft: Radius.circular(_tileRadius), topLeft: Radius.circular(_tileRadius),
@@ -154,11 +148,11 @@ class _PeerCardState extends State<_PeerCard>
), ),
), ),
alignment: Alignment.center, alignment: Alignment.center,
width: isMobile ? 50 : 42, width: isPortrait ? 50 : 42,
height: isMobile ? 50 : null, height: isPortrait ? 50 : null,
child: Stack( child: Stack(
children: [ children: [
getPlatformImage(peer.platform, size: isMobile ? 38 : 30) getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
.paddingAll(6), .paddingAll(6),
if (_shouldBuildPasswordIcon(peer)) if (_shouldBuildPasswordIcon(peer))
Positioned( Positioned(
@@ -183,19 +177,19 @@ class _PeerCardState extends State<_PeerCard>
child: Column( child: Column(
children: [ children: [
Row(children: [ Row(children: [
getOnline(isMobile ? 4 : 8, peer.online), getOnline(isPortrait ? 4 : 8, peer.online),
Expanded( Expanded(
child: Text( child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias, peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).marginOnly(top: isMobile ? 0 : 2), ]).marginOnly(top: isPortrait ? 0 : 2),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
name, name,
style: isMobile ? null : greyStyle, style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start, textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -203,41 +197,47 @@ class _PeerCardState extends State<_PeerCard>
], ],
).marginOnly(top: 2), ).marginOnly(top: 2),
), ),
isMobile isPortrait
? checkBoxOrActionMoreMobile(peer) ? checkBoxOrActionMorePortrait(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true), : checkBoxOrActionMoreLandscape(peer, isTile: true),
], ],
).paddingOnly(left: 10.0, top: 3.0), ).paddingOnly(left: 10.0, top: 3.0),
), ),
) )
], ],
); );
}
Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
hideUsernameOnCard ??=
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
final colors = _frontN(peer.tags, 25) final colors = _frontN(peer.tags, 25)
.map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: peer.tags.isNotEmpty : peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '', : '',
child: Stack(children: [ child: Stack(children: [
deco == null Obx(
? child () => deco == null
: Obx( ? makeChild(stateGlobal.isPortrait.isTrue, peer)
() => Container( : Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
child: child, child: makeChild(stateGlobal.isPortrait.isTrue, peer),
), ),
), ),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Obx(() => Positioned(
top: 2, top: 2,
right: isMobile ? 20 : 10, right: stateGlobal.isPortrait.isTrue ? 20 : 10,
child: CustomPaint( child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors), painter: TagPainter(radius: 3, colors: colors),
), ),
) ))
]), ]),
); );
} }
@@ -253,6 +253,9 @@ class _PeerCardState extends State<_PeerCard>
color: Colors.transparent, color: Colors.transparent,
elevation: 0, elevation: 0,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
// to-do: memory leak here, more investigation needed.
// Continious rebuilds of `Obx()` will cause memory leak here.
// The simple demo does not have this issue.
child: Obx( child: Obx(
() => Container( () => Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
@@ -316,7 +319,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).paddingSymmetric(vertical: 8)), ]).paddingSymmetric(vertical: 8)),
checkBoxOrActionMoreDesktop(peer, isTile: false), checkBoxOrActionMoreLandscape(peer, isTile: false),
], ],
).paddingSymmetric(horizontal: 12.0), ).paddingSymmetric(horizontal: 12.0),
) )
@@ -362,7 +365,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreMobile(Peer peer) { Widget checkBoxOrActionMorePortrait(Peer peer) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@@ -390,7 +393,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) { Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@@ -636,8 +639,8 @@ abstract class BasePeerCard extends StatelessWidget {
@protected @protected
Future<bool> _isForceAlwaysRelay(String id) async { Future<bool> _isForceAlwaysRelay(String id) async {
return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)) return option2bool(kOptionForceAlwaysRelay,
.isNotEmpty; (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay)));
} }
@protected @protected
@@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
@@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
final List favs = (await bind.mainGetFav()).toList(); final List favs = (await bind.mainGetFav()).toList();
@@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard {
BuildContext context) async { BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [ final List<MenuEntryBase<String>> menuItems = [
_connectAction(context), _connectAction(context),
if (!isWeb) _transferFileAction(context), _transferFileAction(context),
]; ];
if (isDesktop && peer.platform != kPeerPlatformAndroid) { if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context)); menuItems.add(_tcpTunnelingAction(context));
@@ -1203,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard {
} }
void _rdpDialog(String id) async { void _rdpDialog(String id) async {
final maxLength = bind.mainMaxEncryptLen();
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
final portController = TextEditingController(text: port); final portController = TextEditingController(text: port);
@@ -1257,54 +1261,54 @@ void _rdpDialog(String id) async {
), ),
], ],
).marginOnly(bottom: isDesktop ? 8 : 0), ).marginOnly(bottom: isDesktop ? 8 : 0),
Row( Obx(() => Row(
children: [ children: [
(isDesktop || isWebDesktop) stateGlobal.isPortrait.isFalse
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
"${translate('Username')}:", "${translate('Username')}:",
textAlign: TextAlign.right, textAlign: TextAlign.right,
).marginOnly(right: 10)) ).marginOnly(right: 10))
: SizedBox.shrink(), : SizedBox.shrink(),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop)
? null
: translate('Username')),
controller: userController,
),
),
],
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0),
Row(
children: [
(isDesktop || isWebDesktop)
? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Password')}:",
textAlign: TextAlign.right,
).marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop) labelText:
? null isDesktop ? null : translate('Username')),
: translate('Password'), controller: userController,
suffixIcon: IconButton( ),
onPressed: () => secure.value = !secure.value, ),
icon: Icon(secure.value ],
? Icons.visibility_off ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
: Icons.visibility))), Obx(() => Row(
controller: passwordController, children: [
)), stateGlobal.isPortrait.isFalse
), ? ConstrainedBox(
], constraints: const BoxConstraints(minWidth: 140),
) child: Text(
"${translate('Password')}:",
textAlign: TextAlign.right,
).marginOnly(right: 10))
: SizedBox.shrink(),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
maxLength: maxLength,
decoration: InputDecoration(
labelText:
isDesktop ? null : translate('Password'),
suffixIcon: IconButton(
onPressed: () =>
secure.value = !secure.value,
icon: Icon(secure.value
? Icons.visibility_off
: Icons.visibility))),
controller: passwordController,
)),
),
],
))
], ],
), ),
), ),

View File

@@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -76,8 +77,11 @@ class _PeerTabPageState extends State<PeerTabPage>
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible); final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
@override _PeerTabPageState() {
void initState() { _loadLocalOptions();
}
void _loadLocalOptions() {
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType); final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
if (uiType != '') { if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == 0 peerCardUiType.value = int.parse(uiType) == 0
@@ -87,8 +91,7 @@ class _PeerTabPageState extends State<PeerTabPage>
: PeerUiType.list; : PeerUiType.list;
} }
hideAbTagsPanel.value = hideAbTagsPanel.value =
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty; bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y';
super.initState();
} }
Future<void> handleTabSelection(int tabIndex) async { Future<void> handleTabSelection(int tabIndex) async {
@@ -105,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
Widget selectionWrap(Widget widget) { Widget selectionWrap(Widget widget) {
return model.multiSelectionMode ? createMultiSelectionBar() : widget; return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
} }
return Column( return Column(
textBaseline: TextBaseline.ideographic, textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Obx(() => SizedBox(
height: 32, height: 32,
child: Container( child: Container(
padding: (isDesktop || isWebDesktop) padding: stateGlobal.isPortrait.isTrue
? null ? EdgeInsets.symmetric(horizontal: 2)
: EdgeInsets.symmetric(horizontal: 2), : null,
child: selectionWrap(Row( child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: child: visibleContextMenuListener(
visibleContextMenuListener(_createSwitchBar(context))), _createSwitchBar(context))),
if (isMobile) if (stateGlobal.isPortrait.isTrue)
..._mobileRightActions(context) ..._portraitRightActions(context)
else else
..._desktopRightActions(context) ..._landscapeRightActions(context)
], ],
)), )),
), ),
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0), ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
_createPeersView(), _createPeersView(),
], ],
); );
@@ -297,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
Widget visibleContextMenuListener(Widget child) { Widget visibleContextMenuListener(Widget child) {
if (isMobile) { if (!(isDesktop || isWebDesktop)) {
return GestureDetector( return GestureDetector(
onLongPressDown: (e) { onLongPressDown: (e) {
final x = e.globalPosition.dx; final x = e.globalPosition.dx;
@@ -359,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
.toList()); .toList());
} }
Widget createMultiSelectionBar() { Widget createMultiSelectionBar(PeerTabModel model) {
final model = Provider.of<PeerTabModel>(context);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -378,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Row( Row(
children: [ children: [
selectionCount(model.selectedPeers.length), selectionCount(model.selectedPeers.length),
selectAll(), selectAll(model),
closeSelection(), closeSelection(),
], ],
) )
@@ -454,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
showToast(translate('Successful')); showToast(translate('Successful'));
}, },
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@@ -475,7 +477,7 @@ class _PeerTabPageState extends State<PeerTabPage>
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
}, },
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]),
).marginOnly(left: isMobile ? 11 : 6), ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@@ -498,7 +500,7 @@ class _PeerTabPageState extends State<PeerTabPage>
}); });
}, },
child: Icon(Icons.tag)) child: Icon(Icons.tag))
.marginOnly(left: isMobile ? 11 : 6), .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@@ -509,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
); );
} }
Widget selectAll() { Widget selectAll(PeerTabModel model) {
final model = Provider.of<PeerTabModel>(context);
return Offstage( return Offstage(
offstage: offstage:
model.selectedPeers.length >= model.currentTabCachedPeers.length, model.selectedPeers.length >= model.currentTabCachedPeers.length,
@@ -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); final model = Provider.of<PeerTabModel>(context);
return [ return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), const PeerSearchBar().marginOnly(right: 13),
_createRefresh( _createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
_createRefresh( _createRefresh(
@@ -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 model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24; final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
@@ -699,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
baseOffset: 0, baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length); extentOffset: peerSearchTextController.value.text.length);
}); });
return Container( return Obx(() => Container(
width: isMobile ? 120 : 140, width: stateGlobal.isPortrait.isTrue ? 120 : 140,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Obx(() => Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Row( child: Row(
@@ -766,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
), ),
) )
], ],
)), ),
); ));
} }
} }
@@ -872,16 +873,18 @@ class PeerSortDropdown extends StatefulWidget {
} }
class _PeerSortDropdownState extends State<PeerSortDropdown> { class _PeerSortDropdownState extends State<PeerSortDropdown> {
@override _PeerSortDropdownState() {
void initState() {
if (!PeerSortType.values.contains(peerSort.value)) { if (!PeerSortType.values.contains(peerSort.value)) {
peerSort.value = PeerSortType.remoteId; _loadLocalOptions();
bind.setLocalFlutterOption(
k: kOptionPeerSorting,
v: peerSort.value,
);
} }
super.initState(); }
void _loadLocalOptions() {
peerSort.value = PeerSortType.remoteId;
bind.setLocalFlutterOption(
k: kOptionPeerSorting,
v: peerSort.value,
);
} }
@override @override

View File

@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
@@ -41,14 +43,26 @@ class LoadEvent {
static const String group = 'load_group_peers'; static const String group = 'load_group_peers';
} }
class PeersModelName {
static const String recent = 'recent peer';
static const String favorite = 'fav peer';
static const String lan = 'discovered peer';
static const String addressBook = 'address book peer';
static const String group = 'group peer';
}
/// for peer search text, global obs value /// for peer search text, global obs value
final peerSearchText = "".obs; final peerSearchText = "".obs;
/// for peer sort, global obs value /// for peer sort, global obs value
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs; RxString? _peerSort;
RxString get peerSort {
_peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
return _peerSort!;
}
// list for listener // list for listener
final obslist = [peerSearchText, peerSort].obs; RxList<RxString> get obslist => [peerSearchText, peerSort].obs;
final peerSearchTextController = final peerSearchTextController =
TextEditingController(text: peerSearchText.value); TextEditingController(text: peerSearchText.value);
@@ -70,7 +84,8 @@ class _PeersView extends StatefulWidget {
} }
/// State for the peer widget. /// State for the peer widget.
class _PeersViewState extends State<_PeersView> with WindowListener { class _PeersViewState extends State<_PeersView>
with WindowListener, WidgetsBindingObserver {
static const int _maxQueryCount = 3; static const int _maxQueryCount = 3;
final HashMap<String, String> _emptyMessages = HashMap.from({ final HashMap<String, String> _emptyMessages = HashMap.from({
LoadEvent.recent: 'empty_recent_tip', LoadEvent.recent: 'empty_recent_tip',
@@ -83,8 +98,10 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{}; var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now(); var _lastQueryTime = DateTime.now();
var _lastWindowRestoreTime = DateTime.now();
var _queryCount = 0; var _queryCount = 0;
var _exit = false; var _exit = false;
bool _isActive = true;
final _scrollController = ScrollController(); final _scrollController = ScrollController();
@@ -95,12 +112,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
@override @override
void initState() { void initState() {
windowManager.addListener(this); windowManager.addListener(this);
WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
windowManager.removeListener(this); windowManager.removeListener(this);
WidgetsBinding.instance.removeObserver(this);
_exit = true; _exit = true;
super.dispose(); super.dispose();
} }
@@ -108,17 +127,61 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
@override @override
void onWindowFocus() { void onWindowFocus() {
_queryCount = 0; _queryCount = 0;
_isActive = true;
}
@override
void onWindowBlur() {
// We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`.
// Maybe it's a bug of the window manager, but the source code seems to be correct.
//
// Although `onWindowRestore()` is called after `onWindowBlur()` in my test,
// we need the following comparison to ensure that `_isActive` is true in the end.
if (isWindows &&
DateTime.now().difference(_lastWindowRestoreTime) <
const Duration(milliseconds: 300)) {
return;
}
_queryCount = _maxQueryCount;
_isActive = false;
}
@override
void onWindowRestore() {
// Window restore (on MacOS and Linux) also triggers `onWindowFocus()`.
// But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager.
if (!isWindows) return;
_queryCount = 0;
_isActive = true;
_lastWindowRestoreTime = DateTime.now();
} }
@override @override
void onWindowMinimize() { void onWindowMinimize() {
_queryCount = _maxQueryCount; // Window minimize also triggers `onWindowBlur()`.
}
// This function is required for mobile.
// `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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<Peers>( // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
create: (context) => widget.peers, // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
// Simple demo can reproduce this issue.
return ChangeNotifierProvider<Peers>.value(
value: widget.peers,
child: Consumer<Peers>(builder: (context, peers, child) { child: Consumer<Peers>(builder: (context, peers, child) {
if (peers.peers.isEmpty) { if (peers.peers.isEmpty) {
gFFI.peerTabModel.setCurrentTabCachedPeers([]); gFFI.peerTabModel.setCurrentTabCachedPeers([]);
@@ -172,7 +235,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
var peers = snapshot.data!; var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000); if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers); gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
buildOnePeer(Peer peer) { buildOnePeer(Peer peer, bool isPortrait) {
final visibilityChild = VisibilityDetector( final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)), key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged, onVisibilityChanged: onVisibilityChanged,
@@ -184,7 +247,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
// No need to listen the currentTab change event. // No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event, // Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView(). // and the peers change event will trigger _buildPeersView().
return (isDesktop || isWebDesktop) return !isPortrait
? Obx(() => peerCardUiType.value == PeerUiType.list ? Obx(() => peerCardUiType.value == PeerUiType.list
? Container(height: 45, child: visibilityChild) ? Container(height: 45, child: visibilityChild)
: peerCardUiType.value == PeerUiType.grid : peerCardUiType.value == PeerUiType.grid
@@ -195,44 +258,45 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
: Container(child: visibilityChild); : Container(child: visibilityChild);
} }
final Widget child; // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
if (isMobile) { // Continious rebuilds of `ListView.builder` will cause memory leak.
child = ListView.builder( // Simple demo can reproduce this issue.
itemCount: peers.length, final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
itemBuilder: (BuildContext context, int index) { ? ListView.builder(
return buildOnePeer(peers[index]).marginOnly( itemCount: peers.length,
top: index == 0 ? 0 : space / 2, bottom: space / 2); itemBuilder: (BuildContext context, int index) {
}, return buildOnePeer(peers[index], true).marginOnly(
); top: index == 0 ? 0 : space / 2, bottom: space / 2);
} else { },
child = Obx(() => peerCardUiType.value == PeerUiType.list )
? DesktopScrollWrapper( : peerCardUiType.value == PeerUiType.list
scrollController: _scrollController, ? DesktopScrollWrapper(
child: ListView.builder( scrollController: _scrollController,
controller: _scrollController, child: ListView.builder(
physics: DraggableNeverScrollableScrollPhysics(), controller: _scrollController,
itemCount: peers.length, physics: DraggableNeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) { itemCount: peers.length,
return buildOnePeer(peers[index]).marginOnly( itemBuilder: (BuildContext context, int index) {
right: space, return buildOnePeer(peers[index], false)
top: index == 0 ? 0 : space / 2, .marginOnly(
bottom: space / 2); right: space,
}), top: index == 0 ? 0 : space / 2,
) bottom: space / 2);
: DesktopScrollWrapper( }),
scrollController: _scrollController, )
child: DynamicGridView.builder( : DesktopScrollWrapper(
controller: _scrollController, scrollController: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(), child: DynamicGridView.builder(
gridDelegate: SliverGridDelegateWithWrapping( controller: _scrollController,
mainAxisSpacing: space / 2, physics: DraggableNeverScrollableScrollPhysics(),
crossAxisSpacing: space), gridDelegate: SliverGridDelegateWithWrapping(
itemCount: peers.length, mainAxisSpacing: space / 2,
itemBuilder: (BuildContext context, int index) { crossAxisSpacing: space),
return buildOnePeer(peers[index]); itemCount: peers.length,
}), itemBuilder: (BuildContext context, int index) {
)); return buildOnePeer(peers[index], false);
} }),
));
if (updateEvent == UpdateEvent.load) { if (updateEvent == UpdateEvent.load) {
_curPeers.clear(); _curPeers.clear();
@@ -268,7 +332,12 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(false); _queryOnlines(false);
} }
} else { } else {
if (_queryCount < _maxQueryCount || !p) { final skipIfIsWeb =
isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage);
final skipIfMobile =
(isAndroid || isIOS) && !stateGlobal.isInMainPage;
final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive;
if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) {
if (now.difference(_lastQueryTime) >= _queryInterval) { if (now.difference(_lastQueryTime) >= _queryInterval) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false)); bind.queryOnlines(ids: _curPeers.toList(growable: false));
@@ -286,14 +355,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
_queryOnlines(bool isLoadEvent) { _queryOnlines(bool isLoadEvent) {
if (_curPeers.isNotEmpty) { if (_curPeers.isNotEmpty) {
bind.queryOnlines(ids: _curPeers.toList(growable: false)); bind.queryOnlines(ids: _curPeers.toList(growable: false));
_lastQueryPeers = {..._curPeers};
if (isLoadEvent) {
_lastChangeTime = DateTime.now();
} else {
_lastQueryTime = DateTime.now().subtract(_queryInterval);
}
_queryCount = 0; _queryCount = 0;
} }
_lastQueryPeers = {..._curPeers};
if (isLoadEvent) {
_lastChangeTime = DateTime.now();
} else {
_lastQueryTime = DateTime.now().subtract(_queryInterval);
}
} }
Future<List<Peer>>? matchPeers( Future<List<Peer>>? matchPeers(
@@ -349,28 +418,39 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
} }
abstract class BasePeersView extends StatelessWidget { abstract class BasePeersView extends StatelessWidget {
final String name; final PeerTabIndex peerTabIndex;
final String loadEvent;
final PeerFilter? peerFilter; final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder; final PeerCardBuilder peerCardBuilder;
final GetInitPeers? getInitPeers;
const BasePeersView({ const BasePeersView({
Key? key, Key? key,
required this.name, required this.peerTabIndex,
required this.loadEvent,
this.peerFilter, this.peerFilter,
required this.peerCardBuilder, required this.peerCardBuilder,
required this.getInitPeers,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Peers peers;
switch (peerTabIndex) {
case PeerTabIndex.recent:
peers = gFFI.recentPeersModel;
break;
case PeerTabIndex.fav:
peers = gFFI.favoritePeersModel;
break;
case PeerTabIndex.lan:
peers = gFFI.lanPeersModel;
break;
case PeerTabIndex.ab:
peers = gFFI.abModel.peersModel;
break;
case PeerTabIndex.group:
peers = gFFI.groupModel.peersModel;
break;
}
return _PeersView( return _PeersView(
peers: peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder);
} }
} }
@@ -379,13 +459,11 @@ class RecentPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'recent peer', peerTabIndex: PeerTabIndex.recent,
loadEvent: LoadEvent.recent,
peerCardBuilder: (Peer peer) => RecentPeerCard( peerCardBuilder: (Peer peer) => RecentPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -401,13 +479,11 @@ class FavoritePeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'favorite peer', peerTabIndex: PeerTabIndex.fav,
loadEvent: LoadEvent.favorite,
peerCardBuilder: (Peer peer) => FavoritePeerCard( peerCardBuilder: (Peer peer) => FavoritePeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -423,13 +499,11 @@ class DiscoveredPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'discovered peer', peerTabIndex: PeerTabIndex.lan,
loadEvent: LoadEvent.lan,
peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -442,21 +516,16 @@ class DiscoveredPeersView extends BasePeersView {
class AddressBookPeersView extends BasePeersView { class AddressBookPeersView extends BasePeersView {
AddressBookPeersView( AddressBookPeersView(
{Key? key, {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
EdgeInsets? menuPadding,
ScrollController? scrollController,
required GetInitPeers getInitPeers})
: super( : super(
key: key, key: key,
name: 'address book peer', peerTabIndex: PeerTabIndex.ab,
loadEvent: LoadEvent.addressBook,
peerFilter: (Peer peer) => peerFilter: (Peer peer) =>
_hitTag(gFFI.abModel.selectedTags, peer.tags), _hitTag(gFFI.abModel.selectedTags, peer.tags),
peerCardBuilder: (Peer peer) => AddressBookPeerCard( peerCardBuilder: (Peer peer) => AddressBookPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: getInitPeers,
); );
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) { static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
@@ -483,20 +552,15 @@ class AddressBookPeersView extends BasePeersView {
class MyGroupPeerView extends BasePeersView { class MyGroupPeerView extends BasePeersView {
MyGroupPeerView( MyGroupPeerView(
{Key? key, {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
EdgeInsets? menuPadding,
ScrollController? scrollController,
required GetInitPeers getInitPeers})
: super( : super(
key: key, key: key,
name: 'group peer', peerTabIndex: PeerTabIndex.group,
loadEvent: LoadEvent.group,
peerFilter: filter, peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard( peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: getInitPeers,
); );
static bool filter(Peer peer) { static bool filter(Peer peer) {

View File

@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// https://github.com/flutter/flutter/issues/154053
final useRawKeyEvents = isLinux && !isWeb;
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
// while `Alt` and `Control` are seperated key events for en-US input method.
return FocusScope( return FocusScope(
autofocus: true, autofocus: true,
child: Focus( child: Focus(
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true, canRequestFocus: true,
focusNode: focusNode, focusNode: focusNode,
onFocusChange: onFocusChange, onFocusChange: onFocusChange,
onKey: (FocusNode data, RawKeyEvent e) => onKey: useRawKeyEvents
inputModel.handleRawKeyEvent(e), ? (FocusNode data, RawKeyEvent event) =>
inputModel.handleRawKeyEvent(event)
: null,
onKeyEvent: useRawKeyEvents
? null
: (FocusNode node, KeyEvent event) =>
inputModel.handleKeyEvent(event),
child: child)); child: child));
} }
} }
@@ -233,7 +243,7 @@ class _RawTouchGestureDetectorRegionState
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return; return;
} }
if (isDesktop) { if (isDesktop || isWebDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords(); ffi.cursorModel.trySetRemoteWindowCoords();
} }
// Workaround for the issue that the first pan event is sent a long time after the start event. // Workaround for the issue that the first pan event is sent a long time after the start event.
@@ -275,7 +285,7 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) { if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords(); ffi.cursorModel.clearRemoteWindowCoords();
} }
inputModel.sendMouse('up', MouseButtons.left); inputModel.sendMouse('up', MouseButtons.left);

View File

@@ -130,12 +130,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// paste // paste
if (isMobile && if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) {
pi.platform != kPeerPlatformAndroid &&
perms['keyboard'] != false &&
perms['clipboard'] != false) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Paste')), child: Text(translate('Send clipboard keystrokes')),
onPressed: () async { onPressed: () async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) { if (data != null && data.text != null) {

View File

@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android"; const String kPeerPlatformAndroid = "Android";
const String kPeerPlatformWebDesktop = "WebDesktop";
const double kScrollbarThickness = 12.0; const double kScrollbarThickness = 12.0;
@@ -136,6 +137,7 @@ const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service"; const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture"; const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification"; const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
// buildin opitons // buildin opitons
const String kOptionHideServerSetting = "hide-server-settings"; const String kOptionHideServerSetting = "hide-server-settings";
@@ -199,7 +201,7 @@ const double kMinFps = 5;
const double kDefaultFps = 30; const double kDefaultFps = 30;
const double kMaxFps = 120; const double kMaxFps = 120;
const double kMinQuality = 10; const double kMinQuality = 5;
const double kDefaultQuality = 50; const double kDefaultQuality = 50;
const double kMaxQuality = 100; const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000; const double kMaxMoreQuality = 2000;
@@ -240,9 +242,9 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0; const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0;
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead. // Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
final kWindowEdgeSize = isWindows ? 1.0 : 5.0; const kWindowResizeEdgeSize = 5.0;
final kWindowBorderWidth = 1.0; const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
const kFrameBorderRadius = 12.0; const kFrameBorderRadius = 12.0;
const kFrameClipRRectBorderRadius = 12.0; const kFrameClipRRectBorderRadius = 12.0;
@@ -568,3 +570,5 @@ enum WindowsTarget {
extension WindowsTargetExt on int { extension WindowsTargetExt on int {
WindowsTarget get windowsVersion => getWindowsTarget(this); WindowsTarget get windowsVersion => getWindowsTarget(this);
} }
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';

View File

@@ -3,8 +3,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -169,16 +169,12 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
final status = final status =
jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>; jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
final statusNum = status['status_num'] as int; final statusNum = status['status_num'] as int;
final preStatus = stateGlobal.svcStatus.value;
if (statusNum == 0) { if (statusNum == 0) {
stateGlobal.svcStatus.value = SvcStatus.connecting; stateGlobal.svcStatus.value = SvcStatus.connecting;
} else if (statusNum == -1) { } else if (statusNum == -1) {
stateGlobal.svcStatus.value = SvcStatus.notReady; stateGlobal.svcStatus.value = SvcStatus.notReady;
} else if (statusNum == 1) { } else if (statusNum == 1) {
stateGlobal.svcStatus.value = SvcStatus.ready; stateGlobal.svcStatus.value = SvcStatus.ready;
if (preStatus != SvcStatus.ready) {
gFFI.userModel.refreshCurrentUser();
}
} else { } else {
stateGlobal.svcStatus.value = SvcStatus.notReady; stateGlobal.svcStatus.value = SvcStatus.notReady;
} }
@@ -212,14 +208,14 @@ class _ConnectionPageState extends State<ConnectionPage>
void initState() { void initState() {
super.initState(); super.initState();
if (_idController.text.isEmpty) { if (_idController.text.isEmpty) {
() async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId(); final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.id) { if (lastRemoteId != _idController.id) {
setState(() { setState(() {
_idController.id = lastRemoteId; _idController.id = lastRemoteId;
}); });
} }
}(); });
} }
Get.put<IDTextEditingController>(_idController); Get.put<IDTextEditingController>(_idController);
windowManager.addListener(this); windowManager.addListener(this);
@@ -261,8 +257,9 @@ class _ConnectionPageState extends State<ConnectionPage>
@override @override
void onWindowLeaveFullScreen() { void onWindowLeaveFullScreen() {
// Restore edge border to default edge size. // Restore edge border to default edge size.
stateGlobal.resizeEdgeSize.value = stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize; ? kMaximizeEdgeSize
: windowResizeEdgeSize;
} }
@override @override
@@ -326,36 +323,7 @@ class _ConnectionPageState extends State<ConnectionPage>
child: Ink( child: Ink(
child: Column( child: Column(
children: [ children: [
Row( getConnectionPageTitle(context, false).marginOnly(bottom: 15),
children: [
Expanded(
child: Row(
children: [
AutoSizeText(
translate('Control Remote Desktop'),
maxLines: 1,
style: Theme.of(context)
.textTheme
.titleLarge
?.merge(TextStyle(height: 1)),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 300),
message: translate("id_input_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)),
],
).marginOnly(bottom: 15),
Row( Row(
children: [ children: [
Expanded( Expanded(

View File

@@ -664,9 +664,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
void initState() { void initState() {
super.initState(); super.initState();
if (!bind.isCustomClient()) { if (!bind.isCustomClient()) {
platformFFI.registerEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
(Map<String, dynamic> evt) async {
if (evt['url'] is String) {
setState(() {
updateUrl = evt['url'];
});
}
});
Timer(const Duration(seconds: 1), () async { Timer(const Duration(seconds: 1), () async {
updateUrl = await bind.mainGetSoftwareUpdateUrl(); bind.mainGetSoftwareUpdateUrl();
if (updateUrl.isNotEmpty) setState(() {});
}); });
} }
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async { _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
@@ -824,6 +832,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
_uniLinksSubscription?.cancel(); _uniLinksSubscription?.cancel();
Get.delete<RxBool>(tag: 'stop-service'); Get.delete<RxBool>(tag: 'stop-service');
_updateTimer?.cancel(); _updateTimer?.cancel();
if (!bind.isCustomClient()) {
platformFFI.unregisterEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
}
super.dispose(); super.dispose();
} }
@@ -857,6 +869,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
// SpecialCharacterValidationRule(), // SpecialCharacterValidationRule(),
MinCharactersValidationRule(8), MinCharactersValidationRule(8),
]; ];
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
submit() { submit() {
@@ -915,6 +928,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg0 = ''; errMsg0 = '';
}); });
}, },
maxLength: maxLength,
), ),
), ),
], ],
@@ -941,6 +955,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg1 = ''; errMsg1 = '';
}); });
}, },
maxLength: maxLength,
), ),
), ),
], ],

View File

@@ -61,7 +61,8 @@ class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey; final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [ static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general, SettingsTabKey.general,
if (!bind.isOutgoingOnly() && if (!isWeb &&
!bind.isOutgoingOnly() &&
!bind.isDisableSettings() && !bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y') bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
SettingsTabKey.safety, SettingsTabKey.safety,
@@ -78,7 +79,8 @@ class DesktopSettingPage extends StatefulWidget {
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key); DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
@override @override
State<DesktopSettingPage> createState() => _DesktopSettingPageState(); State<DesktopSettingPage> createState() =>
_DesktopSettingPageState(initialTabkey);
static void switch2page(SettingsTabKey page) { static void switch2page(SettingsTabKey page) {
try { try {
@@ -111,10 +113,8 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override _DesktopSettingPageState(SettingsTabKey initialTabkey) {
void initState() { var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
super.initState();
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
if (initialIndex == -1) { if (initialIndex == -1) {
initialIndex = 0; initialIndex = 0;
} }
@@ -217,7 +217,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
width: _kTabWidth, width: _kTabWidth,
child: Column( child: Column(
children: [ children: [
_header(), _header(context),
Flexible(child: _listView(tabs: _settingTabs())), Flexible(child: _listView(tabs: _settingTabs())),
], ],
), ),
@@ -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( return Row(
children: [ children: [
SizedBox( if (isWeb)
height: 62, IconButton(
child: Text( onPressed: () {
translate('Settings'), if (Navigator.canPop(context)) {
textAlign: TextAlign.left, Navigator.pop(context);
style: const TextStyle( }
color: _accentColor, },
fontSize: _kTitleFontSize, icon: Icon(Icons.arrow_back),
fontWeight: FontWeight.w400, ).marginOnly(left: 5),
if (isWeb)
SizedBox(
height: 62,
child: Align(
alignment: Alignment.center,
child: settingsText,
), ),
), ).marginOnly(left: 20),
).marginOnly(left: 20, top: 10), if (!isWeb)
SizedBox(
height: 62,
child: settingsText,
).marginOnly(left: 20, top: 10),
const Spacer(), const Spacer(),
], ],
); );
@@ -323,7 +342,8 @@ class _General extends StatefulWidget {
} }
class _GeneralState extends State<_General> { class _GeneralState extends State<_General> {
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service'); final RxBool serviceStop =
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
RxBool serviceBtnEnabled = true.obs; RxBool serviceBtnEnabled = true.obs;
@override @override
@@ -335,13 +355,13 @@ class _GeneralState extends State<_General> {
physics: DraggableNeverScrollableScrollPhysics(), physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController, controller: scrollController,
children: [ children: [
service(), if (!isWeb) service(),
theme(), theme(),
_Card(title: 'Language', children: [language()]), _Card(title: 'Language', children: [language()]),
hwcodec(), if (!isWeb) hwcodec(),
audio(context), if (!isWeb) audio(context),
record(context), if (!isWeb) record(context),
WaylandCard(), if (!isWeb) WaylandCard(),
other() other()
], ],
).marginOnly(bottom: _kListViewBottomMargin)); ).marginOnly(bottom: _kListViewBottomMargin));
@@ -349,8 +369,8 @@ class _GeneralState extends State<_General> {
Widget theme() { Widget theme() {
final current = MyTheme.getThemeModePreference().toShortString(); final current = MyTheme.getThemeModePreference().toShortString();
onChanged(String value) { onChanged(String value) async {
MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
setState(() {}); setState(() {});
} }
@@ -395,13 +415,13 @@ class _GeneralState extends State<_General> {
Widget other() { Widget other() {
final children = <Widget>[ final children = <Widget>[
if (!bind.isIncomingOnly()) if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs', _OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs, kOptionEnableConfirmClosingTabs,
isServer: false), isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
wallpaper(), if (!isWeb) wallpaper(),
if (!bind.isIncomingOnly()) ...[ if (!isWeb && !bind.isIncomingOnly()) ...[
_OptionCheckBox( _OptionCheckBox(
context, context,
'Open connection in new tab', 'Open connection in new tab',
@@ -418,18 +438,19 @@ class _GeneralState extends State<_General> {
kOptionAllowAlwaysSoftwareRender, kOptionAllowAlwaysSoftwareRender,
), ),
), ),
Tooltip( if (!isWeb)
message: translate('texture_render_tip'), Tooltip(
child: _OptionCheckBox( message: translate('texture_render_tip'),
context, child: _OptionCheckBox(
"Use texture rendering", context,
kOptionTextureRender, "Use texture rendering",
optGetter: bind.mainGetUseTextureRender, kOptionTextureRender,
optSetter: (k, v) async => optGetter: bind.mainGetUseTextureRender,
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), optSetter: (k, v) async =>
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
),
), ),
), if (!isWeb && !bind.isCustomClient())
if (!bind.isCustomClient())
_OptionCheckBox( _OptionCheckBox(
context, context,
'Check for software update on startup', 'Check for software update on startup',
@@ -444,7 +465,7 @@ class _GeneralState extends State<_General> {
) )
], ],
]; ];
if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
children.add(_OptionCheckBox( children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless)); context, 'Allow linux headless', kOptionAllowLinuxHeadless));
} }
@@ -516,16 +537,16 @@ class _GeneralState extends State<_General> {
} }
builder(devices, currentDevice, setDevice) { builder(devices, currentDevice, setDevice) {
return _Card(title: 'Audio Input Device', children: [ final child = ComboBox(
...devices.map((device) => _Radio<String>(context, keys: devices,
value: device, values: devices,
groupValue: currentDevice, initialKey: currentDevice,
autoNewLine: false, onChanged: (key) async {
label: device, onChanged: (value) { setDevice(key);
setDevice(value); setState(() {});
setState(() {}); },
})) ).marginOnly(left: _kContentHMargin);
]); return _Card(title: 'Audio Input Device', children: [child]);
} }
return AudioInput(builder: builder, isCm: false, isVoiceCall: false); return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
@@ -642,8 +663,9 @@ class _GeneralState extends State<_General> {
initialKey: currentKey, initialKey: currentKey,
onChanged: (key) async { onChanged: (key) async {
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key); await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
reloadAllWindows(); if (isWeb) reloadCurrentWindow();
bind.mainChangeLanguage(lang: key); if (!isWeb) reloadAllWindows();
if (!isWeb) bind.mainChangeLanguage(lang: key);
}, },
enabled: !isOptFixed, enabled: !isOptFixed,
).marginOnly(left: _kContentHMargin); ).marginOnly(left: _kContentHMargin);
@@ -784,8 +806,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
onChangedBot(!hasBot.value); onChangedBot(!hasBot.value);
}, },
).marginOnly(left: _kCheckBoxLeftMargin + 30); ).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( 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', _OptionCheckBox(context, 'allow-only-conn-window-open-tip',
'allow-only-conn-window-open', 'allow-only-conn-window-open',
reverse: false, enabled: enabled), reverse: false, enabled: enabled),
if (bind.mainIsInstalled()) unlockPin()
]); ]);
} }
@@ -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 { class _Network extends StatefulWidget {
@@ -1278,7 +1360,7 @@ class _Network extends StatefulWidget {
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
bool locked = bind.mainIsInstalled(); bool locked = !isWeb && bind.mainIsInstalled();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -1287,8 +1369,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
final scrollController = ScrollController(); final scrollController = ScrollController();
final hideServer = final hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
// TODO: support web proxy
final hideProxy = final hideProxy =
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
return DesktopScrollWrapper( return DesktopScrollWrapper(
scrollController: scrollController, scrollController: scrollController,
child: ListView( child: ListView(
@@ -1368,8 +1451,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
children: [ children: [
Obx(() => _LabeledTextField(context, 'ID Server', idController, Obx(() => _LabeledTextField(context, 'ID Server', idController,
idErrMsg.value, enabled, secure)), idErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'Relay Server', if (!isWeb)
relayController, relayErrMsg.value, enabled, secure)), Obx(() => _LabeledTextField(context, 'Relay Server',
relayController, relayErrMsg.value, enabled, secure)),
Obx(() => _LabeledTextField(context, 'API Server', Obx(() => _LabeledTextField(context, 'API Server',
apiController, apiErrMsg.value, enabled, secure)), apiController, apiErrMsg.value, enabled, secure)),
_LabeledTextField( _LabeledTextField(
@@ -1408,7 +1492,7 @@ class _DisplayState extends State<_Display> {
scrollStyle(context), scrollStyle(context),
imageQuality(context), imageQuality(context),
codec(context), codec(context),
privacyModeImpl(context), if (!isWeb) privacyModeImpl(context),
other(context), other(context),
]).marginOnly(bottom: _kListViewBottomMargin)); ]).marginOnly(bottom: _kListViewBottomMargin));
} }
@@ -1819,9 +1903,10 @@ class _AboutState extends State<_About> {
SelectionArea( SelectionArea(
child: Text('${translate('Build Date')}: $buildDate') child: Text('${translate('Build Date')}: $buildDate')
.marginSymmetric(vertical: 4.0)), .marginSymmetric(vertical: 4.0)),
SelectionArea( if (!isWeb)
child: Text('${translate('Fingerprint')}: $fingerprint') SelectionArea(
.marginSymmetric(vertical: 4.0)), child: Text('${translate('Fingerprint')}: $fingerprint')
.marginSymmetric(vertical: 4.0)),
InkWell( InkWell(
onTap: () { onTap: () {
launchUrlString('https://rustdesk.com/privacy.html'); launchUrlString('https://rustdesk.com/privacy.html');
@@ -2161,9 +2246,14 @@ Widget _lock(
Text(translate(label)).marginOnly(left: 5), Text(translate(label)).marginOnly(left: 5),
]).marginSymmetric(vertical: 2)), ]).marginSymmetric(vertical: 2)),
onPressed: () async { onPressed: () async {
bool checked = await callMainCheckSuperUserPermission(); final unlockPin = bind.mainGetUnlockPin();
if (checked) { if (unlockPin.isEmpty) {
onUnlock(); bool checked = await callMainCheckSuperUserPermission();
if (checked) {
onUnlock();
}
} else {
checkUnlockPinDialog(unlockPin, onUnlock);
} }
}, },
).marginSymmetric(horizontal: 2, vertical: 4), ).marginSymmetric(horizontal: 2, vertical: 4),
@@ -2423,6 +2513,7 @@ void changeSocks5Proxy() async {
: Icons.visibility))), : Icons.visibility))),
controller: pwdController, controller: pwdController,
enabled: !isOptFixed, enabled: !isOptFixed,
maxLength: bind.mainMaxEncryptLen(),
)), )),
), ),
], ],

View File

@@ -44,21 +44,9 @@ class _DesktopTabPageState extends State<DesktopTabPage>
final RxBool _block = false.obs; final RxBool _block = false.obs;
// bool mouseIn = false; // bool mouseIn = false;
@override _DesktopTabPageState() {
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);
RemoteCountState.init(); RemoteCountState.init();
Get.put<DesktopTabController>(tabController);
tabController.add(TabInfo( tabController.add(TabInfo(
key: kTabLabelHomePage, key: kTabLabelHomePage,
label: 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) { bool _handleKeyEvent(KeyEvent event) {
if (!mouseIn && event is KeyDownEvent) { if (!mouseIn && event is KeyDownEvent) {
@@ -123,6 +126,7 @@ class _DesktopTabPageState extends State<DesktopTabPage>
: Obx( : Obx(
() => DragToResizeArea( () => DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: windowManagerEnableResizeEdges,
child: tabWidget, child: tabWidget,
), ),
); );

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:extended_text/extended_text.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart'; import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
@@ -16,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_hbb/web/dummy.dart'
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
@@ -54,21 +57,21 @@ class FileManagerPage extends StatefulWidget {
required this.id, required this.id,
required this.password, required this.password,
required this.isSharedPassword, required this.isSharedPassword,
required this.tabController, this.tabController,
this.forceRelay}) this.forceRelay})
: super(key: key); : super(key: key);
final String id; final String id;
final String? password; final String? password;
final bool? isSharedPassword; final bool? isSharedPassword;
final bool? forceRelay; final bool? forceRelay;
final DesktopTabController tabController; final DesktopTabController? tabController;
@override @override
State<StatefulWidget> createState() => _FileManagerPageState(); State<StatefulWidget> createState() => _FileManagerPageState();
} }
class _FileManagerPageState extends State<FileManagerPage> class _FileManagerPageState extends State<FileManagerPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none); final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
final _dropMaskVisible = false.obs; // TODO impl drop mask final _dropMaskVisible = false.obs; // TODO impl drop mask
@@ -96,9 +99,16 @@ class _FileManagerPageState extends State<FileManagerPage>
if (!isLinux) { if (!isLinux) {
WakelockPlus.enable(); WakelockPlus.enable();
} }
if (isWeb) {
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
}
debugPrint("File manager page init success with id ${widget.id}"); debugPrint("File manager page init success with id ${widget.id}");
_ffi.dialogManager.setOverlayState(_overlayKeyState); _ffi.dialogManager.setOverlayState(_overlayKeyState);
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 @override
@@ -111,12 +121,21 @@ class _FileManagerPageState extends State<FileManagerPage>
} }
Get.delete<FFI>(tag: 'ft_${widget.id}'); Get.delete<FFI>(tag: 'ft_${widget.id}');
}); });
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
jobController.jobTable.refresh();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
@@ -126,10 +145,11 @@ class _FileManagerPageState extends State<FileManagerPage>
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row( body: Row(
children: [ children: [
Flexible( if (!isWeb)
flex: 3, Flexible(
child: dropArea(FileManagerView( flex: 3,
model.localController, _ffi, _mouseFocusScope))), child: dropArea(FileManagerView(
model.localController, _ffi, _mouseFocusScope))),
Flexible( Flexible(
flex: 3, flex: 3,
child: dropArea(FileManagerView( child: dropArea(FileManagerView(
@@ -170,10 +190,31 @@ class _FileManagerPageState extends State<FileManagerPage>
/// transfer status list /// transfer status list
/// watch transfer status /// watch transfer status
Widget statusList() { Widget statusList() {
Widget getIcon(JobProgress job) {
final color = Theme.of(context).tabBarTheme.labelColor;
switch (job.type) {
case JobType.deleteDir:
case JobType.deleteFile:
return Icon(Icons.delete_outline, color: color);
default:
return Transform.rotate(
angle: isWeb
? job.isRemoteToLocal
? pi / 2
: pi / 2 * 3
: job.isRemoteToLocal
? pi
: 0,
child: Icon(Icons.arrow_forward_ios, color: color),
);
}
}
statusListView(List<JobProgress> jobs) => ListView.builder( statusListView(List<JobProgress> jobs) => ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final item = jobs[index]; final item = jobs[index];
final status = item.getStatus();
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 5), padding: const EdgeInsets.only(bottom: 5),
child: generateCard( child: generateCard(
@@ -183,15 +224,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Transform.rotate( getIcon(item)
angle: item.isRemoteToLocal ? pi : 0, .marginSymmetric(horizontal: 10, vertical: 12),
child: SvgPicture.asset("assets/arrow.svg",
colorFilter: svgColor(
Theme.of(context).tabBarTheme.labelColor)),
).paddingOnly(left: 15),
const SizedBox(
width: 16.0,
),
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -200,45 +234,28 @@ class _FileManagerPageState extends State<FileManagerPage>
Tooltip( Tooltip(
waitDuration: Duration(milliseconds: 500), waitDuration: Duration(milliseconds: 500),
message: item.jobName, message: item.jobName,
child: Text( child: ExtendedText(
item.fileName, item.jobName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).paddingSymmetric(vertical: 10), overflowWidget: TextOverflowWidget(
), child: Text("..."),
Text( position: TextOverflowPosition.start),
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
), ),
), ),
Offstage( Tooltip(
offstage: item.state != JobState.inProgress, waitDuration: Duration(milliseconds: 500),
child: Text( message: status,
'${translate("Speed")} ${readableFileSize(item.speed)}/s', child: Text(status,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: MyTheme.darkGray,
), )).marginOnly(top: 6),
),
), ),
Offstage( Offstage(
offstage: item.state == JobState.inProgress, offstage: item.type != JobType.transfer ||
child: Text( item.state != JobState.inProgress,
translate(
item.display(),
),
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage: item.state != JobState.inProgress,
child: LinearPercentIndicator( child: LinearPercentIndicator(
padding: EdgeInsets.only(right: 15),
animateFromLastPercent: true, animateFromLastPercent: true,
center: Text( center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
@@ -248,7 +265,7 @@ class _FileManagerPageState extends State<FileManagerPage>
progressColor: MyTheme.accent, progressColor: MyTheme.accent,
backgroundColor: Theme.of(context).hoverColor, backgroundColor: Theme.of(context).hoverColor,
lineHeight: kDesktopFileTransferRowHeight, lineHeight: kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15), ).paddingSymmetric(vertical: 8),
), ),
], ],
), ),
@@ -259,6 +276,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Offstage( Offstage(
offstage: item.state != JobState.paused, offstage: item.state != JobState.paused,
child: MenuButton( child: MenuButton(
tooltip: translate("Resume"),
onPressed: () { onPressed: () {
jobController.resumeJob(item.id); jobController.resumeJob(item.id);
}, },
@@ -271,7 +289,7 @@ class _FileManagerPageState extends State<FileManagerPage>
), ),
), ),
MenuButton( MenuButton(
padding: EdgeInsets.only(right: 15), tooltip: translate("Delete"),
child: SvgPicture.asset( child: SvgPicture.asset(
"assets/close.svg", "assets/close.svg",
colorFilter: svgColor(Colors.white), colorFilter: svgColor(Colors.white),
@@ -284,11 +302,11 @@ class _FileManagerPageState extends State<FileManagerPage>
hoverColor: MyTheme.accent80, hoverColor: MyTheme.accent80,
), ),
], ],
), ).marginAll(12),
], ],
), ),
], ],
).paddingSymmetric(vertical: 10), ),
), ),
); );
}, },
@@ -472,6 +490,9 @@ class _FileManagerViewState extends State<FileManagerView> {
} }
Widget headTools() { Widget headTools() {
var uploadButtonTapPosition = RelativeRect.fill;
RxBool isUploadFolder =
(bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs;
return Container( return Container(
child: Column( child: Column(
children: [ children: [
@@ -518,6 +539,7 @@ class _FileManagerViewState extends State<FileManagerView> {
Row( Row(
children: [ children: [
MenuButton( MenuButton(
tooltip: translate('Back'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 3, right: 3,
), ),
@@ -537,6 +559,7 @@ class _FileManagerViewState extends State<FileManagerView> {
}, },
), ),
MenuButton( MenuButton(
tooltip: translate('Parent directory'),
child: RotatedBox( child: RotatedBox(
quarterTurns: 3, quarterTurns: 3,
child: SvgPicture.asset( child: SvgPicture.asset(
@@ -601,6 +624,7 @@ class _FileManagerViewState extends State<FileManagerView> {
switch (_locationStatus.value) { switch (_locationStatus.value) {
case LocationStatus.bread: case LocationStatus.bread:
return MenuButton( return MenuButton(
tooltip: translate('Search'),
onPressed: () { onPressed: () {
_locationStatus.value = LocationStatus.fileSearchBar; _locationStatus.value = LocationStatus.fileSearchBar;
Future.delayed( Future.delayed(
@@ -627,6 +651,7 @@ class _FileManagerViewState extends State<FileManagerView> {
); );
case LocationStatus.fileSearchBar: case LocationStatus.fileSearchBar:
return MenuButton( return MenuButton(
tooltip: translate('Clear'),
onPressed: () { onPressed: () {
onSearchText("", isLocal); onSearchText("", isLocal);
_locationStatus.value = LocationStatus.bread; _locationStatus.value = LocationStatus.bread;
@@ -642,6 +667,7 @@ class _FileManagerViewState extends State<FileManagerView> {
} }
}), }),
MenuButton( MenuButton(
tooltip: translate('Refresh File'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 3, left: 3,
), ),
@@ -667,6 +693,7 @@ class _FileManagerViewState extends State<FileManagerView> {
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [ children: [
MenuButton( MenuButton(
tooltip: translate('Home'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 3, right: 3,
), ),
@@ -682,11 +709,27 @@ class _FileManagerViewState extends State<FileManagerView> {
hoverColor: Theme.of(context).hoverColor, hoverColor: Theme.of(context).hoverColor,
), ),
MenuButton( MenuButton(
tooltip: translate('Create Folder'),
onPressed: () { onPressed: () {
final name = TextEditingController(); final name = TextEditingController();
String? errorText;
_ffi.dialogManager.show((setState, close, context) { _ffi.dialogManager.show((setState, close, context) {
name.addListener(() {
if (errorText != null) {
setState(() {
errorText = null;
});
}
});
submit() { submit() {
if (name.value.text.isNotEmpty) { if (name.value.text.isNotEmpty) {
if (!PathUtil.validName(name.value.text,
controller.options.value.isWindows)) {
setState(() {
errorText = translate("Invalid folder name");
});
return;
}
controller.createDir(PathUtil.join( controller.createDir(PathUtil.join(
controller.directory.value.path, controller.directory.value.path,
name.value.text, name.value.text,
@@ -718,6 +761,7 @@ class _FileManagerViewState extends State<FileManagerView> {
labelText: translate( labelText: translate(
"Please enter the folder name", "Please enter the folder name",
), ),
errorText: errorText,
), ),
controller: name, controller: name,
autofocus: true, autofocus: true,
@@ -751,6 +795,7 @@ class _FileManagerViewState extends State<FileManagerView> {
hoverColor: Theme.of(context).hoverColor, hoverColor: Theme.of(context).hoverColor,
), ),
Obx(() => MenuButton( Obx(() => MenuButton(
tooltip: translate('Delete'),
onPressed: SelectedItems.valid(selectedItems.items) onPressed: SelectedItems.valid(selectedItems.items)
? () async { ? () async {
await (controller await (controller
@@ -770,6 +815,66 @@ class _FileManagerViewState extends State<FileManagerView> {
], ],
), ),
), ),
if (isWeb)
Obx(() => ElevatedButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
isLocal
? EdgeInsets.only(left: 10)
: EdgeInsets.only(right: 10)),
backgroundColor: MaterialStateProperty.all(
selectedItems.items.isEmpty
? MyTheme.accent80
: MyTheme.accent,
),
),
onPressed: () =>
{webselectFiles(is_folder: isUploadFolder.value)},
label: InkWell(
hoverColor: Colors.transparent,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
focusColor: Colors.transparent,
onTapDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
uploadButtonTapPosition =
RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () async {
final value = await showMenu<bool>(
context: context,
position: uploadButtonTapPosition,
items: [
PopupMenuItem<bool>(
value: false,
child: Text(translate('Upload files')),
),
PopupMenuItem<bool>(
value: true,
child: Text(translate('Upload folder')),
),
]);
if (value != null) {
isUploadFolder.value = value;
bind.mainSetLocalOption(
key: 'upload-folder-button',
value: value ? 'Y' : '');
webselectFiles(is_folder: value);
}
},
child: Icon(Icons.arrow_drop_down),
),
icon: Text(
translate(isUploadFolder.isTrue
? 'Upload folder'
: 'Upload files'),
textAlign: TextAlign.right,
style: TextStyle(
color: Colors.white,
),
).marginOnly(left: 8),
)).marginOnly(left: 16),
Obx(() => ElevatedButton.icon( Obx(() => ElevatedButton.icon(
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>( padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
@@ -803,19 +908,22 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white, : Colors.white,
), ),
) )
: RotatedBox( : isWeb
quarterTurns: 2, ? Offstage()
child: SvgPicture.asset( : RotatedBox(
"assets/arrow.svg", quarterTurns: 2,
colorFilter: svgColor(selectedItems.items.isEmpty child: SvgPicture.asset(
? Theme.of(context).brightness == "assets/arrow.svg",
Brightness.light colorFilter: svgColor(
? MyTheme.grayBg selectedItems.items.isEmpty
: MyTheme.darkGray ? Theme.of(context).brightness ==
: Colors.white), Brightness.light
alignment: Alignment.bottomRight, ? MyTheme.grayBg
), : MyTheme.darkGray
), : Colors.white),
alignment: Alignment.bottomRight,
),
),
label: isLocal label: isLocal
? SvgPicture.asset( ? SvgPicture.asset(
"assets/arrow.svg", "assets/arrow.svg",
@@ -827,7 +935,7 @@ class _FileManagerViewState extends State<FileManagerView> {
: Colors.white), : Colors.white),
) )
: Text( : Text(
translate('Receive'), translate(isWeb ? 'Download' : 'Receive'),
style: TextStyle( style: TextStyle(
color: selectedItems.items.isEmpty color: selectedItems.items.isEmpty
? Theme.of(context).brightness == ? Theme.of(context).brightness ==
@@ -882,6 +990,7 @@ class _FileManagerViewState extends State<FileManagerView> {
menuPos = RelativeRect.fromLTRB(x, y, x, y); menuPos = RelativeRect.fromLTRB(x, y, x, y);
}, },
child: MenuButton( child: MenuButton(
tooltip: translate('More'),
onPressed: () => mod_menu.showMenu( onPressed: () => mod_menu.showMenu(
context: context, context: context,
position: menuPos, position: menuPos,
@@ -913,6 +1022,7 @@ class _FileManagerViewState extends State<FileManagerView> {
BuildContext context, ScrollController scrollController) { BuildContext context, ScrollController scrollController) {
final fd = controller.directory.value; final fd = controller.directory.value;
final entries = fd.entries; final entries = fd.entries;
Rx<Entry?> rightClickEntry = Rx(null);
return ListSearchActionListener( return ListSearchActionListener(
node: _keyboardNode, node: _keyboardNode,
@@ -971,16 +1081,70 @@ class _FileManagerViewState extends State<FileManagerView> {
final lastModifiedStr = entry.isDrive final lastModifiedStr = entry.isDrive
? " " ? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} "; : "${entry.lastModified().toString().replaceAll(".000", "")} ";
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
onTap() {
final items = selectedItems;
// handle double click
if (_checkDoubleClick(entry)) {
controller.openDirectory(entry.path);
items.clear();
return;
}
_onSelectedChanged(items, filteredEntries, entry, isLocal);
}
onSecondaryTap() {
final items = [
if (!entry.isDrive &&
versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0)
mod_menu.PopupMenuItem(
child: Text(translate("Rename")),
height: CustomPopupMenuTheme.height,
onTap: () {
controller.renameAction(entry, isLocal);
},
)
];
if (items.isNotEmpty) {
rightClickEntry.value = entry;
final future = mod_menu.showMenu(
context: context,
position: secondaryPosition,
items: items,
);
future.then((value) {
rightClickEntry.value = null;
});
future.onError((error, stackTrace) {
rightClickEntry.value = null;
});
}
}
onSecondaryTapDown(details) {
secondaryPosition = RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy);
}
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(vertical: 1),
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: selectedItems.items.contains(entry) color: selectedItems.items.contains(entry)
? Theme.of(context).hoverColor ? MyTheme.button
: Theme.of(context).cardColor, : Theme.of(context).cardColor,
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(5.0), Radius.circular(5.0),
), ),
border: rightClickEntry.value == entry
? Border.all(
color: MyTheme.button,
width: 1.0,
)
: null,
), ),
key: ValueKey(entry.name), key: ValueKey(entry.name),
height: kDesktopFileTransferRowHeight, height: kDesktopFileTransferRowHeight,
@@ -1019,22 +1183,19 @@ class _FileManagerViewState extends State<FileManagerView> {
), ),
Expanded( Expanded(
child: Text(entry.name.nonBreaking, child: Text(entry.name.nonBreaking,
style: TextStyle(
color: selectedItems.items
.contains(entry)
? Colors.white
: null),
overflow: overflow:
TextOverflow.ellipsis)) TextOverflow.ellipsis))
]), ]),
)), )),
), ),
onTap: () { onTap: onTap,
final items = selectedItems; onSecondaryTap: onSecondaryTap,
// handle double click onSecondaryTapDown: onSecondaryTapDown,
if (_checkDoubleClick(entry)) {
controller.openDirectory(entry.path);
items.clear();
return;
}
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
), ),
SizedBox( SizedBox(
width: 2.0, width: 2.0,
@@ -1051,11 +1212,17 @@ class _FileManagerViewState extends State<FileManagerView> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: selectedItems.items
.contains(entry)
? Colors.white70
: MyTheme.darkGray,
), ),
)), )),
), ),
), ),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
), ),
// Divider from header. // Divider from header.
SizedBox( SizedBox(
@@ -1071,9 +1238,16 @@ class _FileManagerViewState extends State<FileManagerView> {
sizeStr, sizeStr,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 10, color: MyTheme.darkGray), fontSize: 10,
color:
selectedItems.items.contains(entry)
? Colors.white70
: MyTheme.darkGray),
), ),
), ),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
), ),
), ),
], ],

View File

@@ -34,6 +34,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id)); .setTitle(getWindowNameWithId(id));
}; };
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.add(TabInfo( tabController.add(TabInfo(
key: params['id'], key: params['id'],
label: params['id'], label: params['id'],
@@ -54,8 +55,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print( print(
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
@@ -97,6 +96,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
controller: tabController, controller: tabController,
onWindowCloseButton: handleWindowCloseButton, onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(), tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
labelGetter: DesktopTab.tablabelGetter, labelGetter: DesktopTab.tablabelGetter,
)); ));
final tabWidget = isLinux final tabWidget = isLinux
@@ -111,6 +111,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
: SubWindowDragToResizeArea( : SubWindowDragToResizeArea(
child: tabWidget, child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId, windowId: stateGlobal.windowId,
); );
} }

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
@@ -19,9 +21,7 @@ class InstallPage extends StatefulWidget {
class _InstallPageState extends State<InstallPage> { class _InstallPageState extends State<InstallPage> {
final tabController = DesktopTabController(tabType: DesktopTabType.main); final tabController = DesktopTabController(tabType: DesktopTabType.main);
@override _InstallPageState() {
void initState() {
super.initState();
Get.put<DesktopTabController>(tabController); Get.put<DesktopTabController>(tabController);
const label = "install"; const label = "install";
tabController.add(TabInfo( tabController.add(TabInfo(
@@ -43,6 +43,7 @@ class _InstallPageState extends State<InstallPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DragToResizeArea( return DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: windowManagerEnableResizeEdges,
child: Container( child: Container(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
@@ -73,10 +74,16 @@ class _InstallPageBodyState extends State<_InstallPageBody>
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12), padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
); );
_InstallPageBodyState() {
controller = TextEditingController(text: bind.installInstallPath());
final installOptions = jsonDecode(bind.installInstallOptions());
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
}
@override @override
void initState() { void initState() {
windowManager.addListener(this); windowManager.addListener(this);
controller = TextEditingController(text: bind.installInstallPath());
super.initState(); super.initState();
} }
@@ -248,6 +255,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
if (desktopicon.value) args += ' desktopicon'; if (desktopicon.value) args += ' desktopicon';
bind.installInstallMe(options: args, path: controller.text); bind.installInstallMe(options: args, path: controller.text);
} }
do_install(); do_install();
} }

View File

@@ -65,7 +65,10 @@ class _PortForwardPageState extends State<PortForwardPage>
isRdp: widget.isRDP); isRdp: widget.isRDP);
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}'); Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
debugPrint("Port forward page init success with id ${widget.id}"); debugPrint("Port forward page init success with id ${widget.id}");
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 @override

View File

@@ -34,6 +34,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id)); .setTitle(getWindowNameWithId(id));
}; };
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.add(TabInfo( tabController.add(TabInfo(
key: params['id'], key: params['id'],
label: params['id'], label: params['id'],
@@ -54,8 +55,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint( debugPrint(
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId"); "[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
@@ -106,6 +105,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
return true; return true;
}, },
tail: AddButton(), tail: AddButton(),
selectedBorderColor: MyTheme.accent,
labelGetter: DesktopTab.tablabelGetter, labelGetter: DesktopTab.tablabelGetter,
), ),
); );
@@ -127,6 +127,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
() => SubWindowDragToResizeArea( () => SubWindowDragToResizeArea(
child: tabWidget, child: tabWidget,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId, windowId: stateGlobal.windowId,
), ),
); );

View File

@@ -45,7 +45,9 @@ class RemotePage extends StatefulWidget {
this.switchUuid, this.switchUuid,
this.forceRelay, this.forceRelay,
this.isSharedPassword, this.isSharedPassword,
}) : super(key: key); }) : super(key: key) {
initSharedStates(id);
}
final String id; final String id;
final SessionID? sessionId; final SessionID? sessionId;
@@ -64,7 +66,7 @@ class RemotePage extends StatefulWidget {
@override @override
State<RemotePage> createState() { State<RemotePage> createState() {
final state = _RemotePageState(); final state = _RemotePageState(id);
_lastState.value = state; _lastState.value = state;
return state; return state;
} }
@@ -94,8 +96,11 @@ class _RemotePageState extends State<RemotePage>
SessionID get sessionId => _ffi.sessionId; SessionID get sessionId => _ffi.sessionId;
_RemotePageState(String id) {
_initStates(id);
}
void _initStates(String id) { void _initStates(String id) {
initSharedStates(id);
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor); _zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
_showRemoteCursor = ShowRemoteCursorState.find(id); _showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id);
@@ -105,7 +110,6 @@ class _RemotePageState extends State<RemotePage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initStates(widget.id);
_ffi = FFI(widget.sessionId); _ffi = FFI(widget.sessionId);
Get.put<FFI>(_ffi, tag: widget.id); Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) { _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
@@ -135,11 +139,13 @@ class _RemotePageState extends State<RemotePage>
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible(); _ffi.dialogManager.loadMobileActionsOverlayVisible();
// Session option should be set after models.dart/FFI.start WidgetsBinding.instance.addPostFrameCallback((_) {
_showRemoteCursor.value = bind.sessionGetToggleOptionSync( // Session option should be set after models.dart/FFI.start
sessionId: sessionId, arg: 'show-remote-cursor'); _showRemoteCursor.value = bind.sessionGetToggleOptionSync(
_zoomCursor.value = bind.sessionGetToggleOptionSync( sessionId: sessionId, arg: 'show-remote-cursor');
sessionId: sessionId, arg: kOptionZoomCursor); _zoomCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionZoomCursor);
});
DesktopMultiWindow.addListener(this); DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) { // if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback( // customCursorController.registerNeedUpdateCursorCallback(
@@ -154,7 +160,10 @@ class _RemotePageState extends State<RemotePage>
// } // }
_blockableOverlayState.applyFfi(_ffi); _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 @override
@@ -236,8 +245,10 @@ class _RemotePageState extends State<RemotePage>
super.dispose(); super.dispose();
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onRemotePageDispose(closeSession); _ffi.textureModel.onRemotePageDispose(closeSession);
// ensure we leave this session, this is a double check if (closeSession) {
_ffi.inputModel.enterOrLeave(false); // ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);
}
DesktopMultiWindow.removeListener(this); DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay(); _ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage(); _ffi.imageModel.disposeImage();
@@ -565,11 +576,6 @@ class _ImagePaintState extends State<ImagePaint> {
RxBool get remoteCursorMoved => widget.remoteCursorMoved; RxBool get remoteCursorMoved => widget.remoteCursorMoved;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final m = Provider.of<ImageModel>(context); final m = Provider.of<ImageModel>(context);

View File

@@ -71,7 +71,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final ffi = remotePage.ffi; final ffi = remotePage.ffi;
bind.setCurSessionId(sessionId: ffi.sessionId); bind.setCurSessionId(sessionId: ffi.sessionId);
} }
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(params['windowId'])
.setTitle(getWindowNameWithId(id)); .setTitle(getWindowNameWithId(id));
UnreadChatCountState.find(id).value = 0; UnreadChatCountState.find(id).value = 0;
}; };
@@ -98,15 +98,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
)); ));
_update_remote_count(); _update_remote_count();
} }
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
if (!_isScreenRectSet) { if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
restoreWindowPosition( restoreWindowPosition(
@@ -121,11 +120,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
} }
} }
@override
void dispose() {
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final child = Scaffold( final child = Scaffold(
@@ -134,6 +128,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
controller: tabController, controller: tabController,
onWindowCloseButton: handleWindowCloseButton, onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(), tail: const AddButton(),
selectedBorderColor: MyTheme.accent,
pageViewBuilder: (pageView) => pageView, pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.tablabelGetter, labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() { tabBuilder: (key, icon, label, themeConf) => Obx(() {
@@ -233,6 +228,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
// Specially configured for a better resize area and remote control. // Specially configured for a better resize area and remote control.
childPadding: kDragToResizeAreaPadding, childPadding: kDragToResizeAreaPadding,
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
enableResizeEdges: subWindowManagerEnableResizeEdges,
windowId: stateGlobal.windowId, windowId: stateGlobal.windowId,
)); ));
} }
@@ -413,12 +409,14 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final display = args['display']; final display = args['display'];
final displays = args['displays']; final displays = args['displays'];
final screenRect = parseParamScreenRect(args); final screenRect = parseParamScreenRect(args);
final prePeerCount = tabController.length;
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
if (stateGlobal.fullscreen.isTrue) { if (stateGlobal.fullscreen.isTrue) {
await WindowController.fromWindowId(windowId()).setFullscreen(false); await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false); stateGlobal.setFullscreen(false, procWnd: false);
} }
await setNewConnectWindowFrame(windowId(), id!, display, screenRect); await setNewConnectWindowFrame(
windowId(), id!, prePeerCount, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId()); await windowOnTop(windowId());
}); });

View File

@@ -32,14 +32,18 @@ class DesktopServerPage extends StatefulWidget {
class _DesktopServerPageState extends State<DesktopServerPage> class _DesktopServerPageState extends State<DesktopServerPage>
with WindowListener, AutomaticKeepAliveClientMixin { with WindowListener, AutomaticKeepAliveClientMixin {
final tabController = gFFI.serverModel.tabController; final tabController = gFFI.serverModel.tabController;
@override
void initState() { _DesktopServerPageState() {
gFFI.ffiModel.updateEventListener(gFFI.sessionId, ""); gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
windowManager.addListener(this);
Get.put<DesktopTabController>(tabController); Get.put<DesktopTabController>(tabController);
tabController.onRemoved = (_, id) { tabController.onRemoved = (_, id) {
onRemoveId(id); onRemoveId(id);
}; };
}
@override
void initState() {
windowManager.addListener(this);
super.initState(); super.initState();
} }
@@ -79,7 +83,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
child: Consumer<ServerModel>( child: Consumer<ServerModel>(
builder: (context, serverModel, child) { builder: (context, serverModel, child) {
final body = Scaffold( final body = Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).colorScheme.background,
body: ConnectionManager(), body: ConnectionManager(),
); );
return isLinux return isLinux
@@ -108,6 +112,28 @@ class ConnectionManagerState extends State<ConnectionManager>
with WidgetsBindingObserver { with WidgetsBindingObserver {
final RxBool _block = false.obs; 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 @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
@@ -121,25 +147,6 @@ class ConnectionManagerState extends State<ConnectionManager>
@override @override
void initState() { void initState() {
gFFI.serverModel.updateClientState(); 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); WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
} }
@@ -186,8 +193,6 @@ class ConnectionManagerState extends State<ConnectionManager>
maxLabelWidth: 100, maxLabelWidth: 100,
tail: null, //buildScrollJumper(), tail: null, //buildScrollJumper(),
blockTab: allowRemoteCMModification() ? null : _block, blockTab: allowRemoteCMModification() ? null : _block,
selectedTabBackgroundColor:
Theme.of(context).hintColor.withOpacity(0),
tabBuilder: (key, icon, label, themeConf) { tabBuilder: (key, icon, label, themeConf) {
final client = serverModel.clients final client = serverModel.clients
.firstWhereOrNull((client) => client.id.toString() == key); .firstWhereOrNull((client) => client.id.toString() == key);
@@ -222,7 +227,7 @@ class ConnectionManagerState extends State<ConnectionManager>
borderWidth; borderWidth;
final realChatPageWidth = final realChatPageWidth =
constrains.maxWidth - realClosedWidth; constrains.maxWidth - realClosedWidth;
return Row(children: [ final row = Row(children: [
if (constrains.maxWidth > if (constrains.maxWidth >
kConnectionManagerWindowSizeClosedChat.width) kConnectionManagerWindowSizeClosedChat.width)
Consumer<ChatModel>( Consumer<ChatModel>(
@@ -240,6 +245,10 @@ class ConnectionManagerState extends State<ConnectionManager>
child: child:
SizedBox(width: realClosedWidth, child: pageView)), 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; _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 @override
@@ -732,7 +744,8 @@ class _CmControlPanel extends StatelessWidget {
child: buildButton(context, child: buildButton(context,
color: MyTheme.accent, color: MyTheme.accent,
onClick: null, onTapDown: (details) async { onClick: null, onTapDown: (details) async {
final devicesInfo = await AudioInput.getDevicesInfo(true, true); final devicesInfo =
await AudioInput.getDevicesInfo(true, true);
List<String> devices = devicesInfo['devices'] as List<String>; List<String> devices = devicesInfo['devices'] as List<String>;
if (devices.isEmpty) { if (devices.isEmpty) {
msgBox( msgBox(
@@ -764,7 +777,8 @@ class _CmControlPanel extends StatelessWidget {
value: d, value: d,
groupValue: currentDevice, groupValue: currentDevice,
onChanged: (v) { onChanged: (v) {
if (v != null) AudioInput.setDevice(v, true, true); if (v != null)
AudioInput.setDevice(v, true, true);
}, },
child: Container( child: Container(
child: Text( child: Text(
@@ -1143,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
Text(translate('Create Folder')) Text(translate('Create Folder'))
], ],
); );
case CmFileAction.rename:
return Column(
children: [
Icon(
Icons.drive_file_move_outlined,
color: Theme.of(context).tabBarTheme.labelColor,
),
Text(translate('Rename'))
],
);
} }
} }

View File

@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
localPlatform = kPeerPlatformWindows; localPlatform = kPeerPlatformWindows;
} else if (isLinux) { } else if (isLinux) {
localPlatform = kPeerPlatformLinux; localPlatform = kPeerPlatformLinux;
} else if (isWebOnWindows || isWebOnLinux) {
localPlatform = kPeerPlatformWebDesktop;
} }
// to-do: web desktop support ?
return localPlatform; return localPlatform;
} }

View File

@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
return Padding( return Padding(
padding: widget.padding, padding: widget.padding,
child: Tooltip( child: Tooltip(
waitDuration: Duration(milliseconds: 300),
message: widget.tooltip, message: widget.tooltip,
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,

View File

@@ -38,18 +38,16 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
@override @override
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() => MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(); MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>(enabled?.value);
} }
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>> class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
extends State<W> { extends State<W> {
RxBool enabled = true.obs; RxBool enabled = true.obs;
@override MyPopupMenuItemState(bool? e) {
void initState() { if (e != null) {
super.initState(); enabled.value = e;
if (widget.enabled != null) {
enabled.value = widget.enabled!.value;
} }
} }

View File

@@ -372,7 +372,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
initState() { initState() {
super.initState(); super.initState();
Future.delayed(Duration.zero, () async { WidgetsBinding.instance.addPostFrameCallback((_) async {
_fractionX.value = double.tryParse(await bind.sessionGetOption( _fractionX.value = double.tryParse(await bind.sessionGetOption(
sessionId: widget.ffi.sessionId, sessionId: widget.ffi.sessionId,
arg: 'remote-menubar-drag-x') ?? arg: 'remote-menubar-drag-x') ??
@@ -436,6 +436,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
shadowColor: MyTheme.color(context).shadow, shadowColor: MyTheme.color(context).shadow,
borderRadius: borderRadius, borderRadius: borderRadius,
child: _DraggableShowHide( child: _DraggableShowHide(
id: widget.id,
sessionId: widget.ffi.sessionId, sessionId: widget.ffi.sessionId,
dragging: _dragging, dragging: _dragging,
fractionX: _fractionX, fractionX: _fractionX,
@@ -452,8 +453,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
Widget _buildToolbar(BuildContext context) { Widget _buildToolbar(BuildContext context) {
final List<Widget> toolbarItems = []; final List<Widget> toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) { if (!isWebDesktop) {
toolbarItems.add(_PinMenu(state: widget.state));
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
} }
@@ -478,8 +479,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
setFullscreen: _setFullscreen, setFullscreen: _setFullscreen,
)); ));
toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
if (!isWeb) { if (!isWeb) {
toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi));
toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi));
} }
if (!isWeb) toolbarItems.add(_RecordMenu()); if (!isWeb) toolbarItems.add(_RecordMenu());
@@ -1032,11 +1033,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
FFI get ffi => widget.ffi; FFI get ffi => widget.ffi;
String get id => widget.id; String get id => widget.id;
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_screenAdjustor.updateScreen(); _screenAdjustor.updateScreen();
@@ -1278,7 +1274,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_getLocalResolutionWayland(); WidgetsBinding.instance.addPostFrameCallback((_) {
_getLocalResolutionWayland();
});
} }
Rect? scaledRect() { Rect? scaledRect() {
@@ -1615,7 +1613,9 @@ class _KeyboardMenu extends StatelessWidget {
// If use flutter to grab keys, we can only use one mode. // If use flutter to grab keys, we can only use one mode.
// Map mode and Legacy mode, at least one of them is supported. // Map mode and Legacy mode, at least one of them is supported.
String? modeOnly; String? modeOnly;
if (isInputSourceFlutter) { // Keep both map and legacy mode on web at the moment.
// TODO: Remove legacy mode after web supports translate mode on web.
if (isInputSourceFlutter && isDesktop) {
if (bind.sessionIsKeyboardModeSupported( if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyMapMode)) { sessionId: ffi.sessionId, mode: kKeyMapMode)) {
modeOnly = kKeyMapMode; modeOnly = kKeyMapMode;
@@ -1719,7 +1719,9 @@ class _KeyboardMenu extends StatelessWidget {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption( await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly); sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, value); final viewOnly = await bind.sessionGetToggleOption(
sessionId: ffi.sessionId, arg: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, viewOnly ?? value);
} }
: null, : null,
ffi: ffi, ffi: ffi,
@@ -1779,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _IconSubmenuButton( if (isWeb) {
tooltip: 'Chat', return buildTextChatButton();
key: chatButtonKey, } else {
svg: 'assets/chat.svg', return _IconSubmenuButton(
ffi: widget.ffi, tooltip: 'Chat',
color: _ToolbarTheme.blueColor, key: chatButtonKey,
hoverColor: _ToolbarTheme.hoverBlueColor, svg: 'assets/chat.svg',
menuChildrenGetter: () => [textChat(), voiceCall()]); ffi: widget.ffi,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildrenGetter: () => [textChat(), voiceCall()]);
}
}
buildTextChatButton() {
return _IconMenuButton(
assetName: 'assets/message_24dp_5F6368.svg',
tooltip: 'Text chat',
key: chatButtonKey,
onPressed: _textChatOnPressed,
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
);
} }
textChat() { textChat() {
return MenuButton( return MenuButton(
child: Text(translate('Text chat')), child: Text(translate('Text chat')),
ffi: widget.ffi, ffi: widget.ffi,
onPressed: () { onPressed: _textChatOnPressed);
RenderBox? renderBox = }
chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
Offset? initPos; _textChatOnPressed() {
if (renderBox != null) { RenderBox? renderBox =
final pos = renderBox.localToGlobal(Offset.zero); chatButtonKey.currentContext?.findRenderObject() as RenderBox?;
initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); Offset? initPos;
} if (renderBox != null) {
final pos = renderBox.localToGlobal(Offset.zero);
widget.ffi.chatModel.changeCurrentKey( initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
MessageKey(widget.ffi.id, ChatModel.clientModeID)); }
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); widget.ffi.chatModel
}); .changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID));
widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
} }
voiceCall() { voiceCall() {
@@ -2217,6 +2234,7 @@ class RdoMenuButton<T> extends StatelessWidget {
} }
class _DraggableShowHide extends StatefulWidget { class _DraggableShowHide extends StatefulWidget {
final String id;
final SessionID sessionId; final SessionID sessionId;
final RxDouble fractionX; final RxDouble fractionX;
final RxBool dragging; final RxBool dragging;
@@ -2228,6 +2246,7 @@ class _DraggableShowHide extends StatefulWidget {
const _DraggableShowHide({ const _DraggableShowHide({
Key? key, Key? key,
required this.id,
required this.sessionId, required this.sessionId,
required this.fractionX, required this.fractionX,
required this.dragging, required this.dragging,
@@ -2317,15 +2336,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
); );
final isFullscreen = stateGlobal.fullscreen; final isFullscreen = stateGlobal.fullscreen;
const double iconSize = 20; const double iconSize = 20;
buttonWrapper(VoidCallback? onPressed, Widget child,
{Color hoverColor = _ToolbarTheme.blueColor}) {
final bgColor = buttonStyle.backgroundColor?.resolve({});
return TextButton(
onPressed: onPressed,
child: child,
style: buttonStyle.copyWith(
backgroundColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered)) {
return (bgColor ?? hoverColor).withOpacity(0.15);
}
return bgColor;
}),
),
);
}
final child = Row( final child = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildDraggable(context), _buildDraggable(context),
Obx(() => TextButton( Obx(() => buttonWrapper(
onPressed: () { () {
widget.setFullscreen(!isFullscreen.value); widget.setFullscreen(!isFullscreen.value);
}, },
child: Tooltip( Tooltip(
message: translate( message: translate(
isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'), isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'),
child: Icon( child: Icon(
@@ -2336,12 +2373,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
), ),
), ),
)), )),
if (!isMacOS) if (!isMacOS && !isWebDesktop)
Obx(() => Offstage( Obx(() => Offstage(
offstage: isFullscreen.isFalse, offstage: isFullscreen.isFalse,
child: TextButton( child: buttonWrapper(
onPressed: () => widget.setMinimize(), widget.setMinimize,
child: Tooltip( Tooltip(
message: translate('Minimize'), message: translate('Minimize'),
child: Icon( child: Icon(
Icons.remove, Icons.remove,
@@ -2350,11 +2387,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
), ),
), ),
)), )),
TextButton( buttonWrapper(
onPressed: () => setState(() { () => setState(() {
widget.toolbarState.switchShow(widget.sessionId); widget.toolbarState.switchShow(widget.sessionId);
}), }),
child: Obx((() => Tooltip( Obx((() => Tooltip(
message: message:
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon( child: Icon(
@@ -2363,6 +2400,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
), ),
))), ))),
), ),
if (isWebDesktop)
Obx(() {
if (show.isTrue) {
return Offstage();
} else {
return buttonWrapper(
() => closeConnection(id: widget.id),
Tooltip(
message: translate('Close'),
child: Icon(
Icons.close,
size: iconSize,
color: _ToolbarTheme.redColor,
),
),
hoverColor: _ToolbarTheme.redColor,
).paddingOnly(left: iconSize / 2);
}
})
], ],
); );
return TextButtonTheme( return TextButtonTheme(

View File

@@ -227,8 +227,7 @@ typedef TabMenuBuilder = Widget Function(String key);
typedef LabelGetter = Rx<String> Function(String key); typedef LabelGetter = Rx<String> Function(String key);
/// [_lastClickTime], help to handle double click /// [_lastClickTime], help to handle double click
int _lastClickTime = int _lastClickTime = 0;
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
class DesktopTab extends StatefulWidget { class DesktopTab extends StatefulWidget {
final bool showLogo; final bool showLogo;
@@ -506,17 +505,20 @@ class _DesktopTabState extends State<DesktopTab>
Obx(() { Obx(() {
if (stateGlobal.showTabBar.isTrue && if (stateGlobal.showTabBar.isTrue &&
!(kUseCompatibleUiMode && isHideSingleItem())) { !(kUseCompatibleUiMode && isHideSingleItem())) {
final showBottomDivider = _showTabBarBottomDivider(tabType);
return SizedBox( return SizedBox(
height: _kTabBarHeight, height: _kTabBarHeight,
child: Column( child: Column(
children: [ children: [
SizedBox( SizedBox(
height: _kTabBarHeight - 1, height:
showBottomDivider ? _kTabBarHeight - 1 : _kTabBarHeight,
child: _buildBar(), child: _buildBar(),
), ),
const Divider( if (showBottomDivider)
height: 1, const Divider(
), height: 1,
),
], ],
), ),
); );
@@ -550,6 +552,13 @@ class _DesktopTabState extends State<DesktopTab>
controller: state.value.pageController, controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: () { children: () {
if (DesktopTabType.cm == tabType) {
// Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful.
return state.value.tabs.map((tab) {
return tab.page;
}).toList();
}
/// to-do refactor, separate connection state and UI state for remote session. /// to-do refactor, separate connection state and UI state for remote session.
/// [workaround] PageView children need an immutable list, after it has been passed into PageView /// [workaround] PageView children need an immutable list, after it has been passed into PageView
final tabLen = state.value.tabs.length; final tabLen = state.value.tabs.length;
@@ -727,16 +736,6 @@ class WindowActionPanel extends StatefulWidget {
} }
class WindowActionPanelState extends State<WindowActionPanel> { class WindowActionPanelState extends State<WindowActionPanel> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
bool showTabDowndown() { bool showTabDowndown() {
return widget.tabController.state.value.tabs.length > 1 && return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen || (widget.tabController.tabType == DesktopTabType.remoteScreen ||
@@ -1172,7 +1171,10 @@ class _TabState extends State<_Tab> with RestorationMixin {
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
height: _kTabBarHeight, // _kTabBarHeight also displays normally
height: _showTabBarBottomDivider(widget.tabType)
? _kTabBarHeight - 1
: _kTabBarHeight,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@@ -1271,13 +1273,7 @@ class ActionIcon extends StatefulWidget {
} }
class _ActionIconState extends State<ActionIcon> { class _ActionIconState extends State<ActionIcon> {
var hover = false.obs; final hover = false.obs;
@override
void initState() {
super.initState();
hover.value = false;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -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> { class TabbarTheme extends ThemeExtension<TabbarTheme> {
final Color? selectedTabIconColor; final Color? selectedTabIconColor;
final Color? unSelectedTabIconColor; final Color? unSelectedTabIconColor;

View File

@@ -36,6 +36,7 @@ WindowType? kWindowType;
late List<String> kBootArgs; late List<String> kBootArgs;
Future<void> main(List<String> args) async { Future<void> main(List<String> args) async {
earlyAssert();
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
debugPrint("launch args: $args"); debugPrint("launch args: $args");
@@ -161,7 +162,7 @@ void runMobileApp() async {
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
if (!isWeb) await initUniLinks(); await initUniLinks();
} }
void runMultiWindow( void runMultiWindow(
@@ -260,7 +261,7 @@ showCmWindow({bool isStartup = false}) async {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions( WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true); size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
await windowManager.waitUntilReadyToShow(windowOptions, null); await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker(); bind.mainHideDock();
await Future.wait([ await Future.wait([
windowManager.show(), windowManager.show(),
windowManager.focus(), windowManager.focus(),
@@ -288,14 +289,14 @@ hideCmWindow({bool isStartup = false}) async {
size: kConnectionManagerWindowSizeClosedChat); size: kConnectionManagerWindowSizeClosedChat);
windowManager.setOpacity(0); windowManager.setOpacity(0);
await windowManager.waitUntilReadyToShow(windowOptions, null); await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker(); bind.mainHideDock();
await windowManager.minimize(); await windowManager.minimize();
await windowManager.hide(); await windowManager.hide();
_isCmReadyToShow = true; _isCmReadyToShow = true;
} else if (_isCmReadyToShow) { } else if (_isCmReadyToShow) {
if (await windowManager.getOpacity() != 0) { if (await windowManager.getOpacity() != 0) {
await windowManager.setOpacity(0); await windowManager.setOpacity(0);
bind.mainHideDocker(); bind.mainHideDock();
await windowManager.minimize(); await windowManager.minimize();
await windowManager.hide(); await windowManager.hide();
} }
@@ -372,7 +373,7 @@ class App extends StatefulWidget {
State<App> createState() => _AppState(); State<App> createState() => _AppState();
} }
class _AppState extends State<App> { class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -396,6 +397,34 @@ class _AppState extends State<App> {
bind.mainChangeTheme(dark: to.toShortString()); bind.mainChangeTheme(dark: to.toShortString());
} }
}; };
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
_updateOrientation();
}
void _updateOrientation() {
if (isDesktop) return;
// Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
// my test (Flutter 3.19.6, Android 14) is always the reverse value.
// https://github.com/flutter/flutter/issues/60899
// stateGlobal.isPortrait.value =
// MediaQuery.of(context).orientation == Orientation.portrait;
final orientation = View.of(context).physicalSize.aspectRatio > 1
? Orientation.landscape
: Orientation.portrait;
stateGlobal.isPortrait.value = orientation == Orientation.portrait;
} }
@override @override
@@ -416,7 +445,9 @@ class _AppState extends State<App> {
child: GetMaterialApp( child: GetMaterialApp(
navigatorKey: globalKey, navigatorKey: globalKey,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'RustDesk', title: isWeb
? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)'
: bind.mainGetAppNameSync(),
theme: MyTheme.lightTheme, theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme, darkTheme: MyTheme.darkTheme,
themeMode: MyTheme.currentThemeMode(), themeMode: MyTheme.currentThemeMode(),
@@ -447,7 +478,8 @@ class _AppState extends State<App> {
: (context, child) { : (context, child) {
child = _keepScaleBuilder(context, child); child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child); child = botToastBuilder(context, child);
if (isDesktop && desktopType == DesktopType.main) { if ((isDesktop && desktopType == DesktopType.main) ||
isWebDesktop) {
child = keyListenerBuilder(context, child); child = keyListenerBuilder(context, child);
} }
if (isLinux) { if (isLinux) {
@@ -475,7 +507,7 @@ _registerEventHandler() {
platformFFI.registerEventHandler('theme', 'theme', (evt) async { platformFFI.registerEventHandler('theme', 'theme', (evt) async {
String? dark = evt['dark']; String? dark = evt['dark'];
if (dark != null) { if (dark != null) {
MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
} }
}); });
platformFFI.registerEventHandler('language', 'language', (_) async { platformFFI.registerEventHandler('language', 'language', (_) async {

View File

@@ -3,25 +3,23 @@ import 'dart:async';
import 'package:auto_size_text_field/auto_size_text_field.dart'; import 'package:auto_size_text_field/auto_size_text_field.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart'; import '../../common/widgets/autocomplete.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import 'home_page.dart'; import 'home_page.dart';
import 'scan_page.dart';
import 'settings_page.dart';
/// Connection page for connecting to a remote peer. /// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget implements PageShape { class ConnectionPage extends StatefulWidget implements PageShape {
ConnectionPage({Key? key}) : super(key: key); ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
@override @override
final icon = const Icon(Icons.connected_tv); final icon = const Icon(Icons.connected_tv);
@@ -30,7 +28,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
final title = translate("Connection"); final title = translate("Connection");
@override @override
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[]; final List<Widget> appBarActions;
@override @override
State<ConnectionPage> createState() => _ConnectionPageState(); State<ConnectionPage> createState() => _ConnectionPageState();
@@ -50,33 +48,43 @@ class _ConnectionPageState extends State<ConnectionPage> {
bool isPeersLoaded = false; bool isPeersLoaded = false;
StreamSubscription? _uniLinksSubscription; StreamSubscription? _uniLinksSubscription;
_ConnectionPageState() {
if (!isWeb) _uniLinksSubscription = listenUniLinks();
_idController.addListener(() {
_idEmpty.value = _idController.text.isEmpty;
});
Get.put<IDTextEditingController>(_idController);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (!isWeb) _uniLinksSubscription = listenUniLinks();
if (_idController.text.isEmpty) { if (_idController.text.isEmpty) {
() async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId(); final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.id) { if (lastRemoteId != _idController.id) {
setState(() { setState(() {
_idController.id = lastRemoteId; _idController.id = lastRemoteId;
}); });
} }
}(); });
} }
if (isAndroid) { if (isAndroid) {
if (!bind.isCustomClient()) { if (!bind.isCustomClient()) {
platformFFI.registerEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
(Map<String, dynamic> evt) async {
if (evt['url'] is String) {
setState(() {
_updateUrl = evt['url'];
});
}
});
Timer(const Duration(seconds: 1), () async { Timer(const Duration(seconds: 1), () async {
_updateUrl = await bind.mainGetSoftwareUpdateUrl(); bind.mainGetSoftwareUpdateUrl();
if (_updateUrl.isNotEmpty) setState(() {});
}); });
} }
} }
_idController.addListener(() {
_idEmpty.value = _idController.text.isEmpty;
});
Get.put<IDTextEditingController>(_idController);
} }
@override @override
@@ -204,6 +212,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
FocusNode fieldFocusNode, FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) { VoidCallback onFieldSubmitted) {
fieldTextEditingController.text = _idController.text; fieldTextEditingController.text = _idController.text;
Get.put<TextEditingController>(
fieldTextEditingController);
fieldFocusNode.addListener(() async { fieldFocusNode.addListener(() async {
_idEmpty.value = _idEmpty.value =
fieldTextEditingController.text.isEmpty; fieldTextEditingController.text.isEmpty;
@@ -250,6 +260,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
), ),
), ),
inputFormatters: [IDTextInputFormatter()], inputFormatters: [IDTextInputFormatter()],
onSubmitted: (_) {
onConnect();
},
); );
}, },
onSelected: (option) { onSelected: (option) {
@@ -339,9 +352,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
), ),
), ),
); );
final child = Column(children: [
if (isWebDesktop)
getConnectionPageTitle(context, true)
.marginOnly(bottom: 10, top: 15, left: 12),
w
]);
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: Container(constraints: kMobilePageConstraints, child: w)); child: Container(constraints: kMobilePageConstraints, child: child));
} }
@override @override
@@ -351,76 +370,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (Get.isRegistered<IDTextEditingController>()) { if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>(); Get.delete<IDTextEditingController>();
} }
if (Get.isRegistered<TextEditingController>()) {
Get.delete<TextEditingController>();
}
if (!bind.isCustomClient()) {
platformFFI.unregisterEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
}
super.dispose(); super.dispose();
} }
} }
class WebMenu extends StatefulWidget {
const WebMenu({Key? key}) : super(key: key);
@override
State<WebMenu> createState() => _WebMenuState();
}
class _WebMenuState extends State<WebMenu> {
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
return PopupMenuButton<String>(
tooltip: "",
icon: const Icon(Icons.more_vert),
itemBuilder: (context) {
return (isIOS
? [
const PopupMenuItem(
value: "scan",
child: Icon(Icons.qr_code_scanner, color: Colors.black),
)
]
: <PopupMenuItem<String>>[]) +
[
PopupMenuItem(
value: "server",
child: Text(translate('ID/Relay Server')),
)
] +
[
PopupMenuItem(
value: "login",
child: Text(gFFI.userModel.userName.value.isEmpty
? translate("Login")
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
)
] +
[
PopupMenuItem(
value: "about",
child: Text(translate('About RustDesk')),
)
];
},
onSelected: (value) {
if (value == 'server') {
showServerSettings(gFFI.dialogManager);
}
if (value == 'about') {
showAbout(gFFI.dialogManager);
}
if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) {
loginDialog();
} else {
logOutConfirmDialog();
}
}
if (value == 'scan') {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => ScanPage(),
),
);
}
});
}
}

View File

@@ -204,36 +204,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
setState(() {}); setState(() {});
} else if (v == "folder") { } else if (v == "folder") {
final name = TextEditingController(); final name = TextEditingController();
gFFI.dialogManager String? errorText;
.show((setState, close, context) => CustomAlertDialog( gFFI.dialogManager.show((setState, close, context) {
title: Text(translate("Create Folder")), name.addListener(() {
content: Column( if (errorText != null) {
mainAxisSize: MainAxisSize.min, setState(() {
children: [ errorText = null;
TextFormField( });
decoration: InputDecoration( }
labelText: translate( });
"Please enter the folder name"), return CustomAlertDialog(
), title: Text(translate("Create Folder")),
controller: name, content: Column(
), mainAxisSize: MainAxisSize.min,
], children: [
TextFormField(
decoration: InputDecoration(
labelText:
translate("Please enter the folder name"),
errorText: errorText,
), ),
actions: [ controller: name,
dialogButton("Cancel", ),
onPressed: () => close(false), ],
isOutline: true), ),
dialogButton("OK", onPressed: () { actions: [
if (name.value.text.isNotEmpty) { dialogButton("Cancel",
currentFileController.createDir( onPressed: () => close(false), isOutline: true),
PathUtil.join( dialogButton("OK", onPressed: () {
currentDir.path, if (name.value.text.isNotEmpty) {
name.value.text, if (!PathUtil.validName(
currentOptions.isWindows)); name.value.text,
close(); 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") { } else if (v == "hidden") {
currentFileController.toggleShowHidden(); currentFileController.toggleShowHidden();
} }
@@ -497,7 +515,15 @@ class _FileManagerViewState extends State<FileManagerView> {
child: Text(translate("Properties")), child: Text(translate("Properties")),
value: "properties", value: "properties",
enabled: false, enabled: false,
) ),
if (!entries[index].isDrive &&
versionCmp(gFFI.ffiModel.pi.version,
"1.3.0") >=
0)
PopupMenuItem(
child: Text(translate("Rename")),
value: "rename",
)
]; ];
}, },
onSelected: (v) { onSelected: (v) {
@@ -509,6 +535,9 @@ class _FileManagerViewState extends State<FileManagerView> {
_selectedItems.clear(); _selectedItems.clear();
widget.selectMode.toggle(isLocal); widget.selectMode.toggle(isLocal);
setState(() {}); setState(() {});
} else if (v == "rename") {
controller.renameAction(
entries[index], isLocal);
} }
}), }),
onTap: () { onTap: () {

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart'; import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:flutter_hbb/web/settings_page.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/chat_page.dart'; import '../../common/widgets/chat_page.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../../models/state_model.dart';
import 'connection_page.dart'; import 'connection_page.dart';
abstract class PageShape extends Widget { abstract class PageShape extends Widget {
@@ -45,7 +47,11 @@ class HomePageState extends State<HomePage> {
void initPages() { void initPages() {
_pages.clear(); _pages.clear();
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage()); if (!bind.isIncomingOnly()) {
_pages.add(ConnectionPage(
appBarActions: [],
));
}
if (isAndroid && !bind.isOutgoingOnly()) { if (isAndroid && !bind.isOutgoingOnly()) {
_chatPageTabIndex = _pages.length; _chatPageTabIndex = _pages.length;
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
@@ -149,18 +155,80 @@ class HomePageState extends State<HomePage> {
} }
class WebHomePage extends StatelessWidget { class WebHomePage extends StatelessWidget {
final connectionPage = ConnectionPage(); final connectionPage =
ConnectionPage(appBarActions: <Widget>[const WebSettingsPage()]);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
stateGlobal.isInMainPage = true;
handleUnilink(context);
return Scaffold( return Scaffold(
// backgroundColor: MyTheme.grayBg, // backgroundColor: MyTheme.grayBg,
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text(bind.mainGetAppNameSync()), title: Text("${bind.mainGetAppNameSync()} (Preview)"),
actions: connectionPage.appBarActions, actions: connectionPage.appBarActions,
), ),
body: connectionPage, body: connectionPage,
); );
} }
handleUnilink(BuildContext context) {
if (webInitialLink.isEmpty) {
return;
}
final link = webInitialLink;
webInitialLink = '';
final splitter = ["/#/", "/#", "#/", "#"];
var fakelink = '';
for (var s in splitter) {
if (link.contains(s)) {
var list = link.split(s);
if (list.length < 2 || list[1].isEmpty) {
return;
}
list.removeAt(0);
fakelink = "rustdesk://${list.join(s)}";
break;
}
}
if (fakelink.isEmpty) {
return;
}
final uri = Uri.tryParse(fakelink);
if (uri == null) {
return;
}
final args = urlLinkToCmdArgs(uri);
if (args == null || args.isEmpty) {
return;
}
bool isFileTransfer = false;
String? id;
String? password;
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case '--connect':
case '--play':
isFileTransfer = false;
id = args[i + 1];
i++;
break;
case '--file-transfer':
isFileTransfer = true;
id = args[i + 1];
i++;
break;
case '--password':
password = args[i + 1];
i++;
break;
default:
break;
}
}
if (id != null) {
connect(context, id, isFileTransfer: isFileTransfer, password: password);
}
}
} }

View File

@@ -34,7 +34,7 @@ class RemotePage extends StatefulWidget {
final bool? isSharedPassword; final bool? isSharedPassword;
@override @override
State<RemotePage> createState() => _RemotePageState(); State<RemotePage> createState() => _RemotePageState(id);
} }
class _RemotePageState extends State<RemotePage> { class _RemotePageState extends State<RemotePage> {
@@ -57,6 +57,15 @@ class _RemotePageState extends State<RemotePage> {
final TextEditingController _textController = final TextEditingController _textController =
TextEditingController(text: initText); TextEditingController(text: initText);
// This timer is used to check the composing status of the soft keyboard.
// It is used for Android, Korean(and other similar) input method.
Timer? _composingTimer;
_RemotePageState(String id) {
initSharedStates(id);
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
gFFI.dialogManager.loadMobileActionsOverlayVisible();
}
@override @override
void initState() { void initState() {
@@ -80,12 +89,9 @@ class _RemotePageState extends State<RemotePage> {
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
keyboardSubscription = keyboardSubscription =
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged); keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
initSharedStates(widget.id);
gFFI.chatModel gFFI.chatModel
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
_blockableOverlayState.applyFfi(gFFI); _blockableOverlayState.applyFfi(gFFI);
gFFI.dialogManager.loadMobileActionsOverlayVisible();
} }
@override @override
@@ -101,6 +107,7 @@ class _RemotePageState extends State<RemotePage> {
_physicalFocusNode.dispose(); _physicalFocusNode.dispose();
await gFFI.close(); await gFFI.close();
_timer?.cancel(); _timer?.cancel();
_composingTimer?.cancel();
gFFI.dialogManager.dismissAll(); gFFI.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values); overlays: SystemUiOverlay.values);
@@ -136,6 +143,7 @@ class _RemotePageState extends State<RemotePage> {
gFFI.ffiModel.pi.version.isNotEmpty) { gFFI.ffiModel.pi.version.isNotEmpty) {
gFFI.invokeMethod("enable_soft_keyboard", false); gFFI.invokeMethod("enable_soft_keyboard", false);
} }
_composingTimer?.cancel();
} else { } else {
_timer?.cancel(); _timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboardFocus, () { _timer = Timer(kMobileDelaySoftKeyboardFocus, () {
@@ -152,9 +160,9 @@ class _RemotePageState extends State<RemotePage> {
var oldValue = _value; var oldValue = _value;
_value = newValue; _value = newValue;
var i = newValue.length - 1; var i = newValue.length - 1;
for (; i >= 0 && newValue[i] != '\1'; --i) {} for (; i >= 0 && newValue[i] != '1'; --i) {}
var j = oldValue.length - 1; var j = oldValue.length - 1;
for (; j >= 0 && oldValue[j] != '\1'; --j) {} for (; j >= 0 && oldValue[j] != '1'; --j) {}
if (i < j) j = i; if (i < j) j = i;
var subNewValue = newValue.substring(j + 1); var subNewValue = newValue.substring(j + 1);
var subOldValue = oldValue.substring(j + 1); var subOldValue = oldValue.substring(j + 1);
@@ -199,12 +207,19 @@ class _RemotePageState extends State<RemotePage> {
} }
void _handleNonIOSSoftKeyboardInput(String newValue) { void _handleNonIOSSoftKeyboardInput(String newValue) {
_composingTimer?.cancel();
if (_textController.value.isComposingRangeValid) {
_composingTimer = Timer(Duration(milliseconds: 25), () {
_handleNonIOSSoftKeyboardInput(_textController.value.text);
});
return;
}
var oldValue = _value; var oldValue = _value;
_value = newValue; _value = newValue;
if (oldValue.isNotEmpty && if (oldValue.isNotEmpty &&
newValue.isNotEmpty && newValue.isNotEmpty &&
oldValue[0] == '\1' && oldValue[0] == '1' &&
newValue[0] != '\1') { newValue[0] != '1') {
// clipboard // clipboard
oldValue = ''; oldValue = '';
} }
@@ -239,10 +254,14 @@ class _RemotePageState extends State<RemotePage> {
} }
} }
// handle mobile virtual keyboard Future<void> handleSoftKeyboardInput(String newValue) async {
void handleSoftKeyboardInput(String newValue) {
if (isIOS) { if (isIOS) {
_handleIOSSoftKeyboardInput(newValue); // fix: TextFormField onChanged event triggered multiple times when Korean input
// https://github.com/rustdesk/rustdesk/pull/9644
await Future.delayed(const Duration(milliseconds: 10));
if (newValue != _textController.text) return;
_handleIOSSoftKeyboardInput(_textController.text);
} else { } else {
_handleNonIOSSoftKeyboardInput(newValue); _handleNonIOSSoftKeyboardInput(newValue);
} }
@@ -776,11 +795,6 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
onPressed: onPressed); onPressed: onPressed);
} }
@override
void initState() {
super.initState();
}
_updateRect() { _updateRect() {
RenderObject? renderObject = _key.currentContext?.findRenderObject(); RenderObject? renderObject = _key.currentContext?.findRenderObject();
if (renderObject == null) { if (renderObject == null) {

View File

@@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget {
class _ScanPageState extends State<ScanPage> { class _ScanPageState extends State<ScanPage> {
QRViewController? controller; QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
StreamSubscription? scanSubscription;
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
@override @override
void reassemble() { void reassemble() {
super.reassemble(); super.reassemble();
if (isAndroid) { if (isAndroid && controller != null) {
controller!.pauseCamera(); controller!.pauseCamera();
} else if (controller != null) {
controller!.resumeCamera();
} }
controller!.resumeCamera();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Scan QR'), title: const Text('Scan QR'),
actions: [ actions: [
IconButton( _buildImagePickerButton(),
color: Colors.white, _buildFlashToggleButton(),
icon: Icon(Icons.image_search), _buildCameraSwitchButton(),
iconSize: 32.0, ],
onPressed: () async { ),
final ImagePicker picker = ImagePicker(); body: _buildQrView(context),
final XFile? file = );
await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
var image = img.decodeNamedImage(
file.path, File(file.path).readAsBytesSync())!;
LuminanceSource source = RGBLuminanceSource(
image.width,
image.height,
image
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List());
var bitmap = BinaryBitmap(HybridBinarizer(source));
var reader = QRCodeReader();
try {
var result = reader.decode(bitmap);
if (result.text.startsWith(bind.mainUriPrefixSync())) {
handleUriLink(uriString: result.text);
} else {
showServerSettingFromQr(result.text);
}
} catch (e) {
showToast('No QR code found');
}
}
}),
IconButton(
color: Colors.yellow,
icon: Icon(Icons.flash_on),
iconSize: 32.0,
onPressed: () async {
await controller?.toggleFlash();
}),
IconButton(
color: Colors.white,
icon: Icon(Icons.switch_camera),
iconSize: 32.0,
onPressed: () async {
await controller?.flipCamera();
},
),
],
),
body: _buildQrView(context));
} }
Widget _buildQrView(BuildContext context) { Widget _buildQrView(BuildContext context) {
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly. var scanArea = MediaQuery.of(context).size.width < 400 ||
var scanArea = (MediaQuery.of(context).size.width < 400 || MediaQuery.of(context).size.height < 400
MediaQuery.of(context).size.height < 400)
? 150.0 ? 150.0
: 300.0; : 300.0;
// To ensure the Scanner view is properly sizes after rotation
// we need to listen for Flutter SizeChanged notification and update controller
return QRView( return QRView(
key: qrKey, key: qrKey,
onQRViewCreated: _onQRViewCreated, onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape( overlay: QrScannerOverlayShape(
borderColor: Colors.red, borderColor: Colors.red,
borderRadius: 10, borderRadius: 10,
borderLength: 30, borderLength: 30,
borderWidth: 10, borderWidth: 10,
cutOutSize: scanArea), cutOutSize: scanArea,
),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
); );
} }
@@ -116,7 +69,7 @@ class _ScanPageState extends State<ScanPage> {
setState(() { setState(() {
this.controller = controller; this.controller = controller;
}); });
controller.scannedDataStream.listen((scanData) { scanSubscription = controller.scannedDataStream.listen((scanData) {
if (scanData.code != null) { if (scanData.code != null) {
showServerSettingFromQr(scanData.code!); showServerSettingFromQr(scanData.code!);
} }
@@ -129,8 +82,66 @@ class _ScanPageState extends State<ScanPage> {
} }
} }
Future<void> _pickImage() async {
final ImagePicker picker = ImagePicker();
final XFile? file = await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
try {
var image = img.decodeImage(await File(file.path).readAsBytes())!;
LuminanceSource source = RGBLuminanceSource(
image.width,
image.height,
image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(),
);
var bitmap = BinaryBitmap(HybridBinarizer(source));
var reader = QRCodeReader();
var result = reader.decode(bitmap);
if (result.text.startsWith(bind.mainUriPrefixSync())) {
handleUriLink(uriString: result.text);
} else {
showServerSettingFromQr(result.text);
}
} catch (e) {
showToast('No QR code found');
}
}
}
Widget _buildImagePickerButton() {
return IconButton(
color: Colors.white,
icon: Icon(Icons.image_search),
iconSize: 32.0,
onPressed: _pickImage,
);
}
Widget _buildFlashToggleButton() {
return IconButton(
color: Colors.yellow,
icon: Icon(Icons.flash_on),
iconSize: 32.0,
onPressed: () async {
await controller?.toggleFlash();
},
);
}
Widget _buildCameraSwitchButton() {
return IconButton(
color: Colors.white,
icon: Icon(Icons.switch_camera),
iconSize: 32.0,
onPressed: () async {
await controller?.flipCamera();
},
);
}
@override @override
void dispose() { void dispose() {
scanSubscription?.cancel();
controller?.dispose(); controller?.dispose();
super.dispose(); super.dispose();
} }

View File

@@ -187,7 +187,7 @@ class _ServerPageState extends State<ServerPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
buildPresetPasswordWarning(), buildPresetPasswordWarningMobile(),
gFFI.serverModel.isStart gFFI.serverModel.isStart
? ServerInfo() ? ServerInfo()
: ServiceNotRunningNotification(), : ServiceNotRunningNotification(),

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
@@ -87,12 +88,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
var _hideServer = false; var _hideServer = false;
var _hideProxy = false; var _hideProxy = false;
var _hideNetwork = false; var _hideNetwork = false;
var _enableTrustedDevices = false;
@override _SettingsState() {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_enableAbr = option2bool( _enableAbr = option2bool(
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr)); kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery, _denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
@@ -117,8 +115,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; _hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
_hideNetwork = _hideNetwork =
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y'; 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; var update = false;
if (_hasIgnoreBattery) { if (_hasIgnoreBattery) {
@@ -177,7 +182,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
if (update) { if (update) {
setState(() {}); setState(() {});
} }
}(); });
} }
@override @override
@@ -241,18 +246,76 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
], ],
)); ));
final List<AbstractSettingsTile> enhancementsTiles = []; final List<AbstractSettingsTile> enhancementsTiles = [];
final List<AbstractSettingsTile> shareScreenTiles = [ final enable2fa = bind.mainHasValid2FaSync();
final List<AbstractSettingsTile> tfaTiles = [
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('enable-2fa-title')), title: Text(translate('enable-2fa-title')),
initialValue: bind.mainHasValid2FaSync(), initialValue: enable2fa,
onToggle: (_) async { onToggle: (v) async {
update() async { update() async {
setState(() {}); setState(() {});
} }
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( SettingsTile.switchTile(
title: Text(translate('Deny LAN discovery')), title: Text(translate('Deny LAN discovery')),
initialValue: _denyLANDiscovery, 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 && if (isAndroid &&
!disabledSettings && !disabledSettings &&
!outgoingOnly && !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 { class _RadioEntry {
final String label; final String label;
final String value; final String value;

View File

@@ -205,14 +205,15 @@ void showServerSettingsWithValue(
) )
] + ] +
[ [
TextFormField( if (isAndroid)
controller: relayCtrl, TextFormField(
decoration: InputDecoration( controller: relayCtrl,
labelText: translate('Relay Server'), decoration: InputDecoration(
errorText: relayServerMsg.value.isEmpty labelText: translate('Relay Server'),
? null errorText: relayServerMsg.value.isEmpty
: relayServerMsg.value), ? null
) : relayServerMsg.value),
)
] + ] +
[ [
TextFormField( TextFormField(

View File

@@ -41,18 +41,16 @@ class GestureHelp extends StatefulWidget {
final OnTouchModeChange onTouchModeChange; final OnTouchModeChange onTouchModeChange;
@override @override
State<StatefulWidget> createState() => _GestureHelpState(); State<StatefulWidget> createState() => _GestureHelpState(touchMode);
} }
class _GestureHelpState extends State<GestureHelp> { class _GestureHelpState extends State<GestureHelp> {
var _selectedIndex; late int _selectedIndex;
var _touchMode; late bool _touchMode;
@override _GestureHelpState(bool touchMode) {
void initState() { _touchMode = touchMode;
_touchMode = widget.touchMode;
_selectedIndex = _touchMode ? 1 : 0; _selectedIndex = _touchMode ? 1 : 0;
super.initState();
} }
@override @override

View File

@@ -17,17 +17,17 @@ import '../common.dart';
final syncAbOption = 'sync-ab-with-recent-sessions'; final syncAbOption = 'sync-ab-with-recent-sessions';
bool shouldSyncAb() { bool shouldSyncAb() {
return bind.mainGetLocalOption(key: syncAbOption).isNotEmpty; return bind.mainGetLocalOption(key: syncAbOption) == 'Y';
} }
final sortAbTagsOption = 'sync-ab-tags'; final sortAbTagsOption = 'sync-ab-tags';
bool shouldSortTags() { bool shouldSortTags() {
return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty; return bind.mainGetLocalOption(key: sortAbTagsOption) == 'Y';
} }
final filterAbTagOption = 'filter-ab-by-intersection'; final filterAbTagOption = 'filter-ab-by-intersection';
bool filterAbTagByIntersection() { bool filterAbTagByIntersection() {
return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty; return bind.mainGetLocalOption(key: filterAbTagOption) == 'Y';
} }
const _personalAddressBookName = "My address book"; const _personalAddressBookName = "My address book";
@@ -66,10 +66,16 @@ class AbModel {
var listInitialized = false; var listInitialized = false;
var _maxPeerOneAb = 0; var _maxPeerOneAb = 0;
late final Peers peersModel;
WeakReference<FFI> parent; WeakReference<FFI> parent;
AbModel(this.parent) { AbModel(this.parent) {
addressbooks.clear(); addressbooks.clear();
peersModel = Peers(
name: PeersModelName.addressBook,
getInitPeers: () => currentAbPeers,
loadEvent: LoadEvent.addressBook);
if (desktopType == DesktopType.main) { if (desktopType == DesktopType.main) {
Timer.periodic(Duration(milliseconds: 500), (timer) async { Timer.periodic(Duration(milliseconds: 500), (timer) async {
if (_timerCounter++ % 6 == 0) { if (_timerCounter++ % 6 == 0) {
@@ -111,9 +117,10 @@ class AbModel {
Future<void> _pullAb( Future<void> _pullAb(
{required ForcePullAb? force, required bool quiet}) async { {required ForcePullAb? force, required bool quiet}) async {
if (bind.isDisableAb()) return; if (bind.isDisableAb()) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!gFFI.userModel.isLogin) return; if (!gFFI.userModel.isLogin) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (force == null && listInitialized && current.initialized) return; if (force == null && listInitialized && current.initialized) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!listInitialized || force == ForcePullAb.listAndCurrent) { if (!listInitialized || force == ForcePullAb.listAndCurrent) {
try { try {
// Read personal guid every time to avoid upgrading the server without closing the main window // Read personal guid every time to avoid upgrading the server without closing the main window
@@ -815,8 +822,6 @@ abstract class BaseAb {
} }
class LegacyAb extends BaseAb { class LegacyAb extends BaseAb {
final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
bool get emtpy => peers.isEmpty && tags.isEmpty; bool get emtpy => peers.isEmpty && tags.isEmpty;
// licensedDevices is obtained from personal ab, shared ab restrict it in server // licensedDevices is obtained from personal ab, shared ab restrict it in server
var licensedDevices = 0; var licensedDevices = 0;
@@ -1209,8 +1214,6 @@ class LegacyAb extends BaseAb {
class Ab extends BaseAb { class Ab extends BaseAb {
AbProfile profile; AbProfile profile;
late final bool personal; late final bool personal;
final sortTags = shouldSortTags().obs;
final filterByIntersection = filterAbTagByIntersection().obs;
bool get emtpy => peers.isEmpty && tags.isEmpty; bool get emtpy => peers.isEmpty && tags.isEmpty;
Ab(this.profile, this.personal); Ab(this.profile, this.personal);

View File

@@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier {
} }
} }
_isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || _isChatOverlayHide() =>
chatWindowOverlayEntry == null); ((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) ||
chatWindowOverlayEntry == null);
toggleChatOverlay({Offset? chatInitPos}) { toggleChatOverlay({Offset? chatInitPos}) {
if (_isChatOverlayHide()) { if (_isChatOverlayHide()) {
gFFI.invokeMethod("enable_soft_keyboard", true); gFFI.invokeMethod("enable_soft_keyboard", true);
if (!isDesktop) { if (!(isDesktop || isWebDesktop)) {
showChatIconOverlay(); showChatIconOverlay();
} }
showChatWindowOverlay(chatInitPos: chatInitPos); showChatWindowOverlay(chatInitPos: chatInitPos);

View File

@@ -33,6 +33,8 @@ class CmFileModel {
_onFileRemove(evt['remove']); _onFileRemove(evt['remove']);
} else if (evt['create_dir'] != null) { } else if (evt['create_dir'] != null) {
_onDirCreate(evt['create_dir']); _onDirCreate(evt['create_dir']);
} else if (evt['rename'] != null) {
_onRename(evt['rename']);
} }
} }
@@ -59,8 +61,6 @@ class CmFileModel {
_dealOneJob(dynamic l, bool calcSpeed) { _dealOneJob(dynamic l, bool calcSpeed) {
final data = TransferJobSerdeData.fromJson(l); final data = TransferJobSerdeData.fromJson(l);
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
var jobTable = _jobTables[data.connId]; var jobTable = _jobTables[data.connId];
if (jobTable == null) { if (jobTable == null) {
debugPrint("jobTable should not be null"); debugPrint("jobTable should not be null");
@@ -70,12 +70,7 @@ class CmFileModel {
if (job == null) { if (job == null) {
job = CmFileLog(); job = CmFileLog();
jobTable.add(job); jobTable.add(job);
final currentSelectedTab = _addUnread(data.connId);
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
} }
job.id = data.id; job.id = data.id;
job.action = job.action =
@@ -167,8 +162,6 @@ class CmFileModel {
try { try {
dynamic d = jsonDecode(log); dynamic d = jsonDecode(log);
FileActionLog data = FileActionLog.fromJson(d); FileActionLog data = FileActionLog.fromJson(d);
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
var jobTable = _jobTables[data.connId]; var jobTable = _jobTables[data.connId];
if (jobTable == null) { if (jobTable == null) {
debugPrint("jobTable should not be null"); debugPrint("jobTable should not be null");
@@ -179,17 +172,45 @@ class CmFileModel {
..fileName = data.path ..fileName = data.path
..action = CmFileAction.createDir ..action = CmFileAction.createDir
..state = JobState.done); ..state = JobState.done);
final currentSelectedTab = _addUnread(data.connId);
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
jobTable.refresh(); jobTable.refresh();
} catch (e) { } catch (e) {
debugPrint('$e'); debugPrint('$e');
} }
} }
_onRename(dynamic log) {
try {
dynamic d = jsonDecode(log);
FileRenamenLog data = FileRenamenLog.fromJson(d);
var jobTable = _jobTables[data.connId];
if (jobTable == null) {
debugPrint("jobTable should not be null");
return;
}
final fileName = '${data.path} -> ${data.newName}';
jobTable.add(CmFileLog()
..id = 0
..fileName = fileName
..action = CmFileAction.rename
..state = JobState.done);
_addUnread(data.connId);
jobTable.refresh();
} catch (e) {
debugPrint('$e');
}
}
_addUnread(int connId) {
Client? client =
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId);
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == connId.toString())) {
client?.unreadChatMessageCount.value += 1;
}
}
} }
enum CmFileAction { enum CmFileAction {
@@ -198,6 +219,7 @@ enum CmFileAction {
localToRemote, localToRemote,
remove, remove,
createDir, createDir,
rename,
} }
class CmFileLog { class CmFileLog {
@@ -285,3 +307,22 @@ class FileActionLog {
dir: d['dir'] ?? false, dir: d['dir'] ?? false,
); );
} }
class FileRenamenLog {
int connId = 0;
String path = '';
String newName = '';
FileRenamenLog({
required this.connId,
required this.path,
required this.newName,
});
FileRenamenLog.fromJson(dynamic d)
: this(
connId: d['connId'] ?? 0,
path: d['path'] ?? '',
newName: d['newName'] ?? '',
);
}

View File

@@ -181,6 +181,7 @@ class TextureModel {
} }
updateCurrentDisplay(int curDisplay) { updateCurrentDisplay(int curDisplay) {
if (isWeb) return;
final ffi = parent.target; final ffi = parent.target;
if (ffi == null) return; if (ffi == null) return;
tryCreateTexture(int idx) { tryCreateTexture(int idx) {

View File

@@ -3,9 +3,12 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:flutter_hbb/utils/event_loop.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:flutter_hbb/web/dummy.dart'
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
import '../consts.dart'; import '../consts.dart';
import 'model.dart'; import 'model.dart';
@@ -33,6 +36,7 @@ class JobID {
} }
typedef GetSessionID = SessionID Function(); typedef GetSessionID = SessionID Function();
typedef GetDialogManager = OverlayDialogManager? Function();
class FileModel { class FileModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
@@ -44,13 +48,15 @@ class FileModel {
late final FileController remoteController; late final FileController remoteController;
late final GetSessionID getSessionID; late final GetSessionID getSessionID;
late final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID(); SessionID get sessionId => getSessionID();
late final FileDialogEventLoop evtLoop; late final FileDialogEventLoop evtLoop;
FileModel(this.parent) { FileModel(this.parent) {
getSessionID = () => parent.target!.sessionId; getSessionID = () => parent.target!.sessionId;
getDialogManager = () => parent.target?.dialogManager;
fileFetcher = FileFetcher(getSessionID); fileFetcher = FileFetcher(getSessionID);
jobController = JobController(getSessionID); jobController = JobController(getSessionID, getDialogManager);
localController = FileController( localController = FileController(
isLocal: true, isLocal: true,
getSessionID: getSessionID, getSessionID: getSessionID,
@@ -70,7 +76,7 @@ class FileModel {
Future<void> onReady() async { Future<void> onReady() async {
await evtLoop.onReady(); await evtLoop.onReady();
await localController.onReady(); if (!isWeb) await localController.onReady();
await remoteController.onReady(); await remoteController.onReady();
} }
@@ -82,7 +88,7 @@ class FileModel {
} }
Future<void> refreshAll() async { Future<void> refreshAll() async {
await localController.refresh(); if (!isWeb) await localController.refresh();
await remoteController.refresh(); await remoteController.refresh();
} }
@@ -224,6 +230,33 @@ class FileModel {
); );
}, useAnimation: false); }, useAnimation: false);
} }
void onSelectedFiles(dynamic obj) {
localController.selectedItems.clear();
try {
int handleIndex = int.parse(obj['handleIndex']);
final file = jsonDecode(obj['file']);
var entry = Entry.fromJson(file);
entry.path = entry.name;
final otherSideData = remoteController.directoryData();
final toPath = otherSideData.directory.path;
final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden;
final jobID = jobController.addTransferJob(entry, false);
webSendLocalFiles(
handleIndex: handleIndex,
actId: jobID,
path: entry.path,
to: PathUtil.join(toPath, entry.name, isWindows),
fileNum: 0,
includeHidden: showHidden,
isRemote: false,
);
} catch (e) {
debugPrint("Failed to decode onSelectedFiles: $e");
}
}
} }
class DirectoryData { class DirectoryData {
@@ -450,7 +483,7 @@ class FileController {
final isWindows = otherSideData.options.isWindows; final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden; final showHidden = otherSideData.options.showHidden;
for (var from in items.items) { for (var from in items.items) {
final jobID = jobController.add(from, isRemoteToLocal); final jobID = jobController.addTransferJob(from, isRemoteToLocal);
bind.sessionSendFiles( bind.sessionSendFiles(
sessionId: sessionId, sessionId: sessionId,
actId: jobID, actId: jobID,
@@ -458,7 +491,8 @@ class FileController {
to: PathUtil.join(toPath, from.name, isWindows), to: PathUtil.join(toPath, from.name, isWindows),
fileNum: 0, fileNum: 0,
includeHidden: showHidden, includeHidden: showHidden,
isRemote: isRemoteToLocal); isRemote: isRemoteToLocal,
isDir: from.isDirectory);
debugPrint( debugPrint(
"path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}");
} }
@@ -485,7 +519,7 @@ class FileController {
} else if (item.isDirectory) { } else if (item.isDirectory) {
title = translate("Not an empty directory"); title = translate("Not an empty directory");
dialogManager?.showLoading(translate("Waiting")); dialogManager?.showLoading(translate("Waiting"));
final fd = await fileFetcher.fetchDirectoryRecursive( final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
jobID, item.path, items.isLocal, true); jobID, item.path, items.isLocal, true);
if (fd.path.isEmpty) { if (fd.path.isEmpty) {
fd.path = item.path; fd.path = item.path;
@@ -493,13 +527,21 @@ class FileController {
fd.format(isWindows); fd.format(isWindows);
dialogManager?.dismissAll(); dialogManager?.dismissAll();
if (fd.entries.isEmpty) { if (fd.entries.isEmpty) {
var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
final confirm = await showRemoveDialog( final confirm = await showRemoveDialog(
translate( translate(
"Are you sure you want to delete this empty directory?"), "Are you sure you want to delete this empty directory?"),
item.name, item.name,
false); false);
if (confirm == true) { if (confirm == true) {
sendRemoveEmptyDir(item.path, 0); sendRemoveEmptyDir(
item.path,
0,
deleteJobId,
);
} else {
jobController.updateJobStatus(deleteJobId,
error: "cancel", state: JobState.done);
} }
return; return;
} }
@@ -507,6 +549,13 @@ class FileController {
} else { } else {
entries = []; entries = [];
} }
int deleteJobId;
if (item.isDirectory) {
deleteJobId =
jobController.addDeleteDirJob(item, !isLocal, entries.length);
} else {
deleteJobId = jobController.addDeleteFileJob(item, !isLocal);
}
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
final dirShow = item.isDirectory final dirShow = item.isDirectory
@@ -521,24 +570,32 @@ class FileController {
); );
try { try {
if (confirm == true) { if (confirm == true) {
sendRemoveFile(entries[i].path, i); sendRemoveFile(entries[i].path, i, deleteJobId);
final res = await jobController.jobResultListener.start(); final res = await jobController.jobResultListener.start();
// handle remove res; // handle remove res;
if (item.isDirectory && if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) { res['file_num'] == (entries.length - 1).toString()) {
sendRemoveEmptyDir(item.path, i); sendRemoveEmptyDir(item.path, i, deleteJobId);
} }
} else {
jobController.updateJobStatus(deleteJobId,
file_num: i, error: "cancel");
} }
if (_removeCheckboxRemember) { if (_removeCheckboxRemember) {
if (confirm == true) { if (confirm == true) {
for (var j = i + 1; j < entries.length; j++) { for (var j = i + 1; j < entries.length; j++) {
sendRemoveFile(entries[j].path, j); sendRemoveFile(entries[j].path, j, deleteJobId);
final res = await jobController.jobResultListener.start(); final res = await jobController.jobResultListener.start();
if (item.isDirectory && if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) { res['file_num'] == (entries.length - 1).toString()) {
sendRemoveEmptyDir(item.path, i); sendRemoveEmptyDir(item.path, i, deleteJobId);
} }
} }
} else {
jobController.updateJobStatus(deleteJobId,
error: "cancel",
file_num: entries.length,
state: JobState.done);
} }
break; break;
} }
@@ -617,22 +674,19 @@ class FileController {
}, useAnimation: false); }, useAnimation: false);
} }
void sendRemoveFile(String path, int fileNum) { void sendRemoveFile(String path, int fileNum, int actId) {
bind.sessionRemoveFile( bind.sessionRemoveFile(
sessionId: sessionId, sessionId: sessionId,
actId: JobController.jobID.next(), actId: actId,
path: path, path: path,
isRemote: !isLocal, isRemote: !isLocal,
fileNum: fileNum); fileNum: fileNum);
} }
void sendRemoveEmptyDir(String path, int fileNum) { void sendRemoveEmptyDir(String path, int fileNum, int actId) {
history.removeWhere((element) => element.contains(path)); history.removeWhere((element) => element.contains(path));
bind.sessionRemoveAllEmptyDirs( bind.sessionRemoveAllEmptyDirs(
sessionId: sessionId, sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
actId: JobController.jobID.next(),
path: path,
isRemote: !isLocal);
} }
Future<void> createDir(String path) async { Future<void> createDir(String path) async {
@@ -642,29 +696,102 @@ class FileController {
path: path, path: path,
isRemote: !isLocal); isRemote: !isLocal);
} }
Future<void> renameAction(Entry item, bool isLocal) async {
final textEditingController = TextEditingController(text: item.name);
String? errorText;
dialogManager?.show((setState, close, context) {
textEditingController.addListener(() {
if (errorText != null) {
setState(() {
errorText = null;
});
}
});
submit() async {
final newName = textEditingController.text;
if (newName.isEmpty || newName == item.name) {
close();
return;
}
if (directory.value.entries.any((e) => e.name == newName)) {
setState(() {
errorText = translate("Already exists");
});
return;
}
if (!PathUtil.validName(newName, options.value.isWindows)) {
setState(() {
if (item.isDirectory) {
errorText = translate("Invalid folder name");
} else {
errorText = translate("Invalid file name");
}
});
return;
}
await bind.sessionRenameFile(
sessionId: sessionId,
actId: JobController.jobID.next(),
path: item.path,
newName: newName,
isRemote: !isLocal);
close();
}
return CustomAlertDialog(
content: Column(
children: [
DialogTextField(
title: '${translate('Rename')} ${item.name}',
controller: textEditingController,
errorText: errorText,
),
],
),
actions: [
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: close,
isOutline: true,
),
dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: submit,
),
],
onSubmit: submit,
onCancel: close,
);
});
}
} }
const _kOneWayFileTransferError = 'one-way-file-transfer-tip';
class JobController { class JobController {
static final JobID jobID = JobID(); static final JobID jobID = JobID();
final jobTable = List<JobProgress>.empty(growable: true).obs; final jobTable = List<JobProgress>.empty(growable: true).obs;
final jobResultListener = JobResultListener<Map<String, dynamic>>(); final jobResultListener = JobResultListener<Map<String, dynamic>>();
final GetSessionID getSessionID; final GetSessionID getSessionID;
final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID(); SessionID get sessionId => getSessionID();
OverlayDialogManager? get alogManager => getDialogManager();
int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
JobController(this.getSessionID); JobController(this.getSessionID, this.getDialogManager);
int getJob(int id) { int getJob(int id) {
return jobTable.indexWhere((element) => element.id == id); return jobTable.indexWhere((element) => element.id == id);
} }
// JobProgress? getJob(int id) {
// return jobTable.firstWhere((element) => element.id == id);
// }
// return jobID // return jobID
int add(Entry from, bool isRemoteToLocal) { int addTransferJob(Entry from, bool isRemoteToLocal) {
final jobID = JobController.jobID.next(); final jobID = JobController.jobID.next();
jobTable.add(JobProgress() jobTable.add(JobProgress()
..type = JobType.transfer
..fileName = path.basename(from.path) ..fileName = path.basename(from.path)
..jobName = from.path ..jobName = from.path
..totalSize = from.size ..totalSize = from.size
@@ -674,6 +801,33 @@ class JobController {
return jobID; return jobID;
} }
int addDeleteFileJob(Entry file, bool isRemote) {
final jobID = JobController.jobID.next();
jobTable.add(JobProgress()
..type = JobType.deleteFile
..fileName = path.basename(file.path)
..jobName = file.path
..totalSize = file.size
..state = JobState.none
..id = jobID
..isRemoteToLocal = isRemote);
return jobID;
}
int addDeleteDirJob(Entry file, bool isRemote, int fileCount) {
final jobID = JobController.jobID.next();
jobTable.add(JobProgress()
..type = JobType.deleteDir
..fileName = path.basename(file.path)
..jobName = file.path
..fileCount = fileCount
..totalSize = file.size
..state = JobState.none
..id = jobID
..isRemoteToLocal = isRemote);
return jobID;
}
void tryUpdateJobProgress(Map<String, dynamic> evt) { void tryUpdateJobProgress(Map<String, dynamic> evt) {
try { try {
int id = int.parse(evt['id']); int id = int.parse(evt['id']);
@@ -684,7 +838,7 @@ class JobController {
job.fileNum = int.parse(evt['file_num']); job.fileNum = int.parse(evt['file_num']);
job.speed = double.parse(evt['speed']); job.speed = double.parse(evt['speed']);
job.finishedSize = int.parse(evt['finished_size']); job.finishedSize = int.parse(evt['finished_size']);
debugPrint("update job $id with $evt"); job.recvJobRes = true;
jobTable.refresh(); jobTable.refresh();
} }
} catch (e) { } catch (e) {
@@ -692,20 +846,48 @@ class JobController {
} }
} }
void jobDone(Map<String, dynamic> evt) async { Future<bool> jobDone(Map<String, dynamic> evt) async {
if (jobResultListener.isListening) { if (jobResultListener.isListening) {
jobResultListener.complete(evt); jobResultListener.complete(evt);
return; // return;
} }
int id = -1;
int id = int.parse(evt['id']); int? fileNum = 0;
double? speed = 0;
try {
id = int.parse(evt['id']);
} catch (_) {}
final jobIndex = getJob(id); final jobIndex = getJob(id);
if (jobIndex != -1) { if (jobIndex == -1) return true;
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
job.finishedSize = job.totalSize; job.recvJobRes = true;
if (job.type == JobType.deleteFile) {
job.state = JobState.done; job.state = JobState.done;
job.fileNum = int.parse(evt['file_num']); } else if (job.type == JobType.deleteDir) {
jobTable.refresh(); try {
fileNum = int.tryParse(evt['file_num']);
} catch (_) {}
if (fileNum != null) {
if (fileNum < job.fileNum) return true; // file_num can be 0 at last
job.fileNum = fileNum;
if (fileNum >= job.fileCount - 1) {
job.state = JobState.done;
}
}
} else {
try {
fileNum = int.tryParse(evt['file_num']);
speed = double.tryParse(evt['speed']);
} catch (_) {}
if (fileNum != null) job.fileNum = fileNum;
if (speed != null) job.speed = speed;
job.state = JobState.done;
}
jobTable.refresh();
if (job.type == JobType.deleteDir) {
return job.state == JobState.done;
} else {
return true;
} }
} }
@@ -716,16 +898,61 @@ class JobController {
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
job.state = JobState.error; job.state = JobState.error;
job.err = err; job.err = err;
job.fileNum = int.parse(evt['file_num']); job.recvJobRes = true;
if (err == "skipped") { if (job.type == JobType.transfer) {
job.state = JobState.done; int? fileNum = int.tryParse(evt['file_num']);
job.finishedSize = job.totalSize; if (fileNum != null) job.fileNum = fileNum;
if (err == "skipped") {
job.state = JobState.done;
job.finishedSize = job.totalSize;
}
} else if (job.type == JobType.deleteDir) {
if (jobResultListener.isListening) {
jobResultListener.complete(evt);
}
int? fileNum = int.tryParse(evt['file_num']);
if (fileNum != null) job.fileNum = fileNum;
} else if (job.type == JobType.deleteFile) {
if (jobResultListener.isListening) {
jobResultListener.complete(evt);
}
} }
jobTable.refresh(); jobTable.refresh();
} }
if (err == _kOneWayFileTransferError) {
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
final dm = alogManager;
if (dm != null) {
_lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch;
msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm);
}
}
}
debugPrint("jobError $evt"); debugPrint("jobError $evt");
} }
void updateJobStatus(int id,
{int? file_num, String? error, JobState? state}) {
final jobIndex = getJob(id);
if (jobIndex < 0) return;
final job = jobTable[jobIndex];
job.recvJobRes = true;
if (file_num != null) {
job.fileNum = file_num;
}
if (error != null) {
job.err = error;
job.state = JobState.error;
}
if (state != null) {
job.state = state;
}
if (job.type == JobType.deleteFile && error == null) {
job.state = JobState.done;
}
jobTable.refresh();
}
Future<void> cancelJob(int id) async { Future<void> cancelJob(int id) async {
await bind.sessionCancelJob(sessionId: sessionId, actId: id); await bind.sessionCancelJob(sessionId: sessionId, actId: id);
} }
@@ -742,6 +969,7 @@ class JobController {
final currJobId = JobController.jobID.next(); final currJobId = JobController.jobID.next();
String fileName = path.basename(isRemote ? remote : to); String fileName = path.basename(isRemote ? remote : to);
var jobProgress = JobProgress() var jobProgress = JobProgress()
..type = JobType.transfer
..fileName = fileName ..fileName = fileName
..jobName = isRemote ? remote : to ..jobName = isRemote ? remote : to
..id = currJobId ..id = currJobId
@@ -917,11 +1145,11 @@ class FileFetcher {
} }
} }
Future<FileDirectory> fetchDirectoryRecursive( Future<FileDirectory> fetchDirectoryRecursiveToRemove(
int actID, String path, bool isLocal, bool showHidden) async { int actID, String path, bool isLocal, bool showHidden) async {
// TODO test Recursive is show hidden default? // TODO test Recursive is show hidden default?
try { try {
await bind.sessionReadDirRecursive( await bind.sessionReadDirToRemoveRecursive(
sessionId: sessionId, sessionId: sessionId,
actId: actID, actId: actID,
path: path, path: path,
@@ -1016,8 +1244,12 @@ extension JobStateDisplay on JobState {
} }
} }
enum JobType { none, transfer, deleteFile, deleteDir }
class JobProgress { class JobProgress {
JobType type = JobType.none;
JobState state = JobState.none; JobState state = JobState.none;
var recvJobRes = false;
var id = 0; var id = 0;
var fileNum = 0; var fileNum = 0;
var speed = 0.0; var speed = 0.0;
@@ -1037,7 +1269,9 @@ class JobProgress {
int lastTransferredSize = 0; int lastTransferredSize = 0;
clear() { clear() {
type = JobType.none;
state = JobState.none; state = JobState.none;
recvJobRes = false;
id = 0; id = 0;
fileNum = 0; fileNum = 0;
speed = 0; speed = 0;
@@ -1051,11 +1285,81 @@ class JobProgress {
} }
String display() { String display() {
if (state == JobState.done && err == "skipped") { if (type == JobType.transfer) {
return translate("Skipped"); if (state == JobState.done && err == "skipped") {
return translate("Skipped");
}
} else if (type == JobType.deleteFile) {
if (err == "cancel") {
return translate("Cancel");
}
} }
return state.display(); return state.display();
} }
String getStatus() {
int handledFileCount = recvJobRes ? fileNum + 1 : fileNum;
if (handledFileCount >= fileCount) {
handledFileCount = fileCount;
}
if (state == JobState.done) {
handledFileCount = fileCount;
finishedSize = totalSize;
}
final filesStr = "$handledFileCount/$fileCount files";
final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : "";
final sizePercentStr = totalSize > 0 && finishedSize > 0
? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}"
: "";
if (type == JobType.deleteFile) {
return display();
} else if (type == JobType.deleteDir) {
var res = '';
if (state == JobState.done || state == JobState.error) {
res = display();
}
if (filesStr.isNotEmpty) {
if (res.isNotEmpty) {
res += " ";
}
res += filesStr;
}
if (sizeStr.isNotEmpty) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizeStr;
}
return res;
} else if (type == JobType.transfer) {
var res = "";
if (state != JobState.inProgress && state != JobState.none) {
res += display();
}
if (filesStr.isNotEmpty) {
if (res.isNotEmpty) {
res += ", ";
}
res += filesStr;
}
if (sizeStr.isNotEmpty && state != JobState.inProgress) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizeStr;
}
if (sizePercentStr.isNotEmpty && state == JobState.inProgress) {
if (res.isNotEmpty) {
res += ", ";
}
res += sizePercentStr;
}
return res;
}
return '';
}
} }
class _PathStat { class _PathStat {
@@ -1083,6 +1387,13 @@ class PathUtil {
final pathUtil = isWindows ? windowsContext : posixContext; final pathUtil = isWindows ? windowsContext : posixContext;
return pathUtil.dirname(path); return pathUtil.dirname(path);
} }
static bool validName(String name, bool isWindows) {
final unixFileNamePattern = RegExp(r'^[^/\0]+$');
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
return reg.hasMatch(name);
}
} }
class DirectoryOptions { class DirectoryOptions {

View File

@@ -23,11 +23,19 @@ class GroupModel {
bool get emtpy => users.isEmpty && peers.isEmpty; bool get emtpy => users.isEmpty && peers.isEmpty;
GroupModel(this.parent); late final Peers peersModel;
GroupModel(this.parent) {
peersModel = Peers(
name: PeersModelName.group,
getInitPeers: () => peers,
loadEvent: LoadEvent.group);
}
Future<void> pull({force = true, quiet = false}) async { Future<void> pull({force = true, quiet = false}) async {
if (bind.isDisableGroupPanel()) return; if (bind.isDisableGroupPanel()) return;
if (!gFFI.userModel.isLogin || groupLoading.value) return; if (!gFFI.userModel.isLogin || groupLoading.value) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (!force && initialized) return; if (!force && initialized) return;
if (!quiet) { if (!quiet) {
groupLoading.value = true; groupLoading.value = true;

View File

@@ -177,7 +177,7 @@ class PointerEventToRust {
} }
} }
class ToReleaseKeys { class ToReleaseRawKeys {
RawKeyEvent? lastLShiftKeyEvent; RawKeyEvent? lastLShiftKeyEvent;
RawKeyEvent? lastRShiftKeyEvent; RawKeyEvent? lastRShiftKeyEvent;
RawKeyEvent? lastLCtrlKeyEvent; RawKeyEvent? lastLCtrlKeyEvent;
@@ -282,6 +282,48 @@ class ToReleaseKeys {
} }
} }
class ToReleaseKeys {
KeyEvent? lastLShiftKeyEvent;
KeyEvent? lastRShiftKeyEvent;
KeyEvent? lastLCtrlKeyEvent;
KeyEvent? lastRCtrlKeyEvent;
KeyEvent? lastLAltKeyEvent;
KeyEvent? lastRAltKeyEvent;
KeyEvent? lastLCommandKeyEvent;
KeyEvent? lastRCommandKeyEvent;
KeyEvent? lastSuperKeyEvent;
reset() {
lastLShiftKeyEvent = null;
lastRShiftKeyEvent = null;
lastLCtrlKeyEvent = null;
lastRCtrlKeyEvent = null;
lastLAltKeyEvent = null;
lastRAltKeyEvent = null;
lastLCommandKeyEvent = null;
lastRCommandKeyEvent = null;
lastSuperKeyEvent = null;
}
release(KeyEventResult Function(KeyEvent e) handleKeyEvent) {
for (final key in [
lastLShiftKeyEvent,
lastRShiftKeyEvent,
lastLCtrlKeyEvent,
lastRCtrlKeyEvent,
lastLAltKeyEvent,
lastRAltKeyEvent,
lastLCommandKeyEvent,
lastRCommandKeyEvent,
lastSuperKeyEvent,
]) {
if (key != null) {
handleKeyEvent(key);
}
}
}
}
class InputModel { class InputModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
String keyboardMode = ''; String keyboardMode = '';
@@ -292,6 +334,7 @@ class InputModel {
var alt = false; var alt = false;
var command = false; var command = false;
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
final ToReleaseKeys toReleaseKeys = ToReleaseKeys(); final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
// trackpad // trackpad
@@ -339,10 +382,99 @@ class InputModel {
} }
} }
void handleKeyDownEventModifiers(KeyEvent e) {
KeyUpEvent upEvent(e) => KeyUpEvent(
physicalKey: e.physicalKey,
logicalKey: e.logicalKey,
timeStamp: e.timeStamp,
);
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
if (!alt) {
alt = true;
}
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
if (!alt) {
alt = true;
}
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
if (!ctrl) {
ctrl = true;
}
toReleaseKeys.lastLCtrlKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
if (!ctrl) {
ctrl = true;
}
toReleaseKeys.lastRCtrlKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
if (!shift) {
shift = true;
}
toReleaseKeys.lastLShiftKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
if (!shift) {
shift = true;
}
toReleaseKeys.lastRShiftKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
if (!command) {
command = true;
}
toReleaseKeys.lastLCommandKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
if (!command) {
command = true;
}
toReleaseKeys.lastRCommandKeyEvent = upEvent(e);
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
if (!command) {
command = true;
}
toReleaseKeys.lastSuperKeyEvent = upEvent(e);
}
}
void handleKeyUpEventModifiers(KeyEvent e) {
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
alt = false;
toReleaseKeys.lastLAltKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
alt = false;
toReleaseKeys.lastRAltKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
ctrl = false;
toReleaseKeys.lastLCtrlKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
ctrl = false;
toReleaseKeys.lastRCtrlKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
shift = false;
toReleaseKeys.lastLShiftKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
shift = false;
toReleaseKeys.lastRShiftKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
command = false;
toReleaseKeys.lastLCommandKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
command = false;
toReleaseKeys.lastRCommandKeyEvent = null;
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
command = false;
toReleaseKeys.lastSuperKeyEvent = null;
}
}
KeyEventResult handleRawKeyEvent(RawKeyEvent e) { KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled; if (isViewOnly) return KeyEventResult.handled;
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { if (!isInputSourceFlutter) {
return KeyEventResult.handled; if (isDesktop) {
return KeyEventResult.handled;
} else if (isWeb) {
return KeyEventResult.ignored;
}
} }
final key = e.logicalKey; final key = e.logicalKey;
@@ -358,7 +490,7 @@ class InputModel {
command = true; command = true;
} }
} }
toReleaseKeys.updateKeyDown(key, e); toReleaseRawKeys.updateKeyDown(key, e);
} }
if (e is RawKeyUpEvent) { if (e is RawKeyUpEvent) {
if (key == LogicalKeyboardKey.altLeft || if (key == LogicalKeyboardKey.altLeft ||
@@ -376,12 +508,50 @@ class InputModel {
command = false; command = false;
} }
toReleaseKeys.updateKeyUp(key, e); toReleaseRawKeys.updateKeyUp(key, e);
} }
// * Currently mobile does not enable map mode // * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == 'map') { if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardMode(e); mapKeyboardModeRaw(e);
} else {
legacyKeyboardModeRaw(e);
}
return KeyEventResult.handled;
}
KeyEventResult handleKeyEvent(KeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if (!isInputSourceFlutter) {
if (isDesktop) {
return KeyEventResult.handled;
} else if (isWeb) {
return KeyEventResult.ignored;
}
}
if (isWindows || isLinux) {
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||
e.physicalKey == PhysicalKeyboardKey.metaRight) {
return KeyEventResult.handled;
}
}
if (e is KeyUpEvent) {
handleKeyUpEventModifiers(e);
} else if (e is KeyDownEvent) {
handleKeyDownEventModifiers(e);
}
// * 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 { } else {
legacyKeyboardMode(e); legacyKeyboardMode(e);
} }
@@ -389,7 +559,33 @@ class InputModel {
return KeyEventResult.handled; return KeyEventResult.handled;
} }
void mapKeyboardMode(RawKeyEvent e) { /// Send Key Event
void newKeyboardMode(String character, int usbHid, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
bind.sessionHandleFlutterKeyEvent(
sessionId: sessionId,
character: character,
usbHid: usbHid,
lockModes: lockModes,
downOrUp: down);
}
void mapKeyboardModeRaw(RawKeyEvent e) {
int positionCode = -1; int positionCode = -1;
int platformCode = -1; int platformCode = -1;
bool down; bool down;
@@ -441,7 +637,7 @@ class InputModel {
.contains(KeyboardLockMode.scrollLock)) { .contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock); lockModes |= (1 << scrolllock);
} }
bind.sessionHandleFlutterKeyEvent( bind.sessionHandleFlutterRawKeyEvent(
sessionId: sessionId, sessionId: sessionId,
name: name, name: name,
platformCode: platformCode, platformCode: platformCode,
@@ -450,7 +646,7 @@ class InputModel {
downOrUp: down); downOrUp: down);
} }
void legacyKeyboardMode(RawKeyEvent e) { void legacyKeyboardModeRaw(RawKeyEvent e) {
if (e is RawKeyDownEvent) { if (e is RawKeyDownEvent) {
if (e.repeat) { if (e.repeat) {
sendRawKey(e, press: true); sendRawKey(e, press: true);
@@ -471,6 +667,24 @@ class InputModel {
inputKey(label, down: down, press: press ?? false); inputKey(label, down: down, press: press ?? false);
} }
void legacyKeyboardMode(KeyEvent e) {
if (e is KeyDownEvent) {
sendKey(e, down: true);
} else if (e is KeyRepeatEvent) {
sendKey(e, press: true);
} else if (e is KeyUpEvent) {
sendKey(e);
}
}
void sendKey(KeyEvent e, {bool? down, bool? press}) {
// for maximum compatibility
final label = physicalKeyMap[e.physicalKey.usbHidUsage] ??
logicalKeyMap[e.logicalKey.keyId] ??
e.logicalKey.keyLabel;
inputKey(label, down: down, press: press ?? false);
}
/// Send key stroke event. /// Send key stroke event.
/// [down] indicates the key's state(down or up). /// [down] indicates the key's state(down or up).
/// [press] indicates a click event(down and up). /// [press] indicates a click event(down and up).
@@ -566,7 +780,8 @@ class InputModel {
} }
void enterOrLeave(bool enter) { void enterOrLeave(bool enter) {
toReleaseKeys.release(handleRawKeyEvent); toReleaseKeys.release(handleKeyEvent);
toReleaseRawKeys.release(handleRawKeyEvent);
_pointerMovedAfterEnter = false; _pointerMovedAfterEnter = false;
// Fix status // Fix status
@@ -577,6 +792,9 @@ class InputModel {
if (!isInputSourceFlutter) { if (!isInputSourceFlutter) {
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter); bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
} }
if (!isWeb && enter) {
bind.setCurSessionId(sessionId: sessionId);
}
} }
/// Send mouse movement event with distance in [x] and [y]. /// Send mouse movement event with distance in [x] and [y].
@@ -1164,15 +1382,15 @@ class InputModel {
// Simulate a key press event. // Simulate a key press event.
// `usbHidUsage` is the USB HID usage code of the key. // `usbHidUsage` is the USB HID usage code of the key.
Future<void> tapHidKey(int usbHidUsage) async { Future<void> tapHidKey(int usbHidUsage) async {
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true); newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
await Future.delayed(Duration(milliseconds: 100)); await Future.delayed(Duration(milliseconds: 100));
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false); newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
} }
Future<void> onMobileVolumeUp() async => Future<void> onMobileVolumeUp() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF);
Future<void> onMobileVolumeDown() async => Future<void> onMobileVolumeDown() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF);
Future<void> onMobilePower() async => Future<void> onMobilePower() async =>
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF);
} }

View File

@@ -4,15 +4,18 @@ import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/cm_file_model.dart'; import 'package:flutter_hbb/models/cm_file_model.dart';
import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart'; import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/user_model.dart';
@@ -267,6 +270,8 @@ class FfiModel with ChangeNotifier {
var name = evt['name']; var name = evt['name'];
if (name == 'msgbox') { if (name == 'msgbox') {
handleMsgBox(evt, sessionId, peerId); handleMsgBox(evt, sessionId, peerId);
} else if (name == 'toast') {
handleToast(evt, sessionId, peerId);
} else if (name == 'set_multiple_windows_session') { } else if (name == 'set_multiple_windows_session') {
handleMultipleWindowsSession(evt, sessionId, peerId); handleMultipleWindowsSession(evt, sessionId, peerId);
} else if (name == 'peer_info') { } else if (name == 'peer_info') {
@@ -304,8 +309,13 @@ class FfiModel with ChangeNotifier {
} else if (name == 'job_progress') { } else if (name == 'job_progress') {
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
} else if (name == 'job_done') { } else if (name == 'job_done') {
parent.target?.fileModel.jobController.jobDone(evt); bool? refresh =
parent.target?.fileModel.refreshAll(); await parent.target?.fileModel.jobController.jobDone(evt);
if (refresh == true) {
// many job done for delete directory
// todo: refresh may not work when confirm delete local directory
parent.target?.fileModel.refreshAll();
}
} else if (name == 'job_error') { } else if (name == 'job_error') {
parent.target?.fileModel.jobController.jobError(evt); parent.target?.fileModel.jobController.jobError(evt);
} else if (name == 'override_file_confirm') { } else if (name == 'override_file_confirm') {
@@ -365,7 +375,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'plugin_option') { } else if (name == 'plugin_option') {
handleOption(evt); handleOption(evt);
} else if (name == "sync_peer_hash_password_to_personal_ab") { } else if (name == "sync_peer_hash_password_to_personal_ab") {
if (desktopType == DesktopType.main) { if (desktopType == DesktopType.main || isWeb) {
final id = evt['id']; final id = evt['id'];
final hash = evt['hash']; final hash = evt['hash'];
if (id != null && hash != null) { if (id != null && hash != null) {
@@ -383,8 +393,12 @@ class FfiModel with ChangeNotifier {
handleFollowCurrentDisplay(evt, sessionId, peerId); handleFollowCurrentDisplay(evt, sessionId, peerId);
} else if (name == 'use_texture_render') { } else if (name == 'use_texture_render') {
_handleUseTextureRender(evt, sessionId, peerId); _handleUseTextureRender(evt, sessionId, peerId);
} else if (name == "selected_files") {
if (isWeb) {
parent.target?.fileModel.onSelectedFiles(evt);
}
} else { } else {
debugPrint('Unknown event name: $name'); debugPrint('Event is not handled in the fixed branch: $name');
} }
}; };
} }
@@ -492,10 +506,12 @@ class FfiModel with ChangeNotifier {
newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width; newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height; newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1; newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
newDisplay.originalWidth = newDisplay.originalWidth = int.tryParse(
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; evt['original_width'] ?? kInvalidResolutionValue.toString()) ??
newDisplay.originalHeight = kInvalidResolutionValue;
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; newDisplay.originalHeight = int.tryParse(
evt['original_height'] ?? kInvalidResolutionValue.toString()) ??
kInvalidResolutionValue;
newDisplay._scale = _pi.scaleOfDisplay(display); newDisplay._scale = _pi.scaleOfDisplay(display);
_pi.displays[display] = newDisplay; _pi.displays[display] = newDisplay;
@@ -582,6 +598,37 @@ class FfiModel with ChangeNotifier {
} }
} }
handleToast(Map<String, dynamic> evt, SessionID sessionId, String peerId) {
final type = evt['type'] ?? 'info';
final text = evt['text'] ?? '';
final durMsc = evt['dur_msec'] ?? 2000;
final duration = Duration(milliseconds: durMsc);
if ((text).isEmpty) {
BotToast.showLoading(
duration: duration,
clickClose: true,
allowClick: true,
);
} else {
if (type.contains('error')) {
BotToast.showText(
contentColor: Colors.red,
text: translate(text),
duration: duration,
clickClose: true,
onlyOne: true,
);
} else {
BotToast.showText(
text: translate(text),
duration: duration,
clickClose: true,
onlyOne: true,
);
}
}
}
/// Show a message box with [type], [title] and [text]. /// Show a message box with [type], [title] and [text].
showMsgBox(SessionID sessionId, String type, String title, String text, showMsgBox(SessionID sessionId, String type, String title, String text,
String link, bool hasRetry, OverlayDialogManager dialogManager, String link, bool hasRetry, OverlayDialogManager dialogManager,
@@ -723,6 +770,8 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt]. /// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async { handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
parent.target?.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
// This call is to ensuer the keyboard mode is updated depending on the peer version. // This call is to ensuer the keyboard mode is updated depending on the peer version.
parent.target?.inputModel.updateKeyboardMode(); parent.target?.inputModel.updateKeyboardMode();
@@ -786,7 +835,7 @@ class FfiModel with ChangeNotifier {
isRefreshing = false; isRefreshing = false;
} }
Map<String, dynamic> features = json.decode(evt['features']); Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1; _pi.features.privacyMode = features['privacy_mode'] == true;
if (!isCache) { if (!isCache) {
handleResolutions(peerId, evt["resolutions"]); handleResolutions(peerId, evt["resolutions"]);
} }
@@ -830,7 +879,7 @@ class FfiModel with ChangeNotifier {
for (final mode in [kKeyMapMode, kKeyLegacyMode]) { for (final mode in [kKeyMapMode, kKeyLegacyMode]) {
if (bind.sessionIsKeyboardModeSupported( if (bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: mode)) { sessionId: sessionId, mode: mode)) {
bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
break; break;
} }
} }
@@ -1001,14 +1050,15 @@ class FfiModel with ChangeNotifier {
// Notify to switch display // Notify to switch display
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt', msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
'display_is_plugged_out_msg', '', parent.target!.dialogManager); 'display_is_plugged_out_msg', '', parent.target!.dialogManager);
final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex final isPeerPrimaryDisplayValid =
? 0 pi.primaryDisplay == kInvalidDisplayIndex ||
: pi.primaryDisplay; pi.primaryDisplay >= pi.displays.length;
final displays = newDisplay; final newDisplay =
isPeerPrimaryDisplayValid ? 0 : pi.primaryDisplay;
bind.sessionSwitchDisplay( bind.sessionSwitchDisplay(
isDesktop: isDesktop, isDesktop: isDesktop,
sessionId: sessionId, sessionId: sessionId,
value: Int32List.fromList([displays]), value: Int32List.fromList([newDisplay]),
); );
if (_pi.isSupportMultiUiSession) { if (_pi.isSupportMultiUiSession) {
@@ -1173,26 +1223,49 @@ class ImageModel with ChangeNotifier {
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
onRgba(int display, Uint8List rgba) { clearImage() => _image = null;
bool _webDecodingRgba = false;
final List<Uint8List> _webRgbaList = List.empty(growable: true);
webOnRgba(int display, Uint8List rgba) async {
// deep copy needed, otherwise "instantiateCodec failed: TypeError: Cannot perform Construct on a detached ArrayBuffer"
_webRgbaList.add(Uint8List.fromList(rgba));
if (_webDecodingRgba) {
return;
}
_webDecodingRgba = true;
try {
while (_webRgbaList.isNotEmpty) {
final rgba2 = _webRgbaList.last;
_webRgbaList.clear();
await decodeAndUpdate(display, rgba2);
}
} catch (e) {
debugPrint('onRgba error: $e');
}
_webDecodingRgba = false;
}
onRgba(int display, Uint8List rgba) async {
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 pid = parent.target?.id;
final rect = parent.target?.ffiModel.pi.getDisplayRect(display); final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
img.decodeImageFromPixels( final image = await img.decodeImageFromPixels(
rgba, rgba,
rect?.width.toInt() ?? 0, rect?.width.toInt() ?? 0,
rect?.height.toInt() ?? 0, rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
onPixelsCopied: () { );
// Unlock the rgba memory from rust codes. if (parent.target?.id != pid) return;
platformFFI.nextRgba(sessionId, display); await update(image);
}).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');
}
});
} }
update(ui.Image? image) async { update(ui.Image? image) async {
@@ -2173,6 +2246,7 @@ class CursorModel with ChangeNotifier {
debugPrint("deleting cursor with key $k"); debugPrint("deleting cursor with key $k");
deleteCustomCursor(k); deleteCustomCursor(k);
} }
resetSystemCursor();
} }
trySetRemoteWindowCoords() { trySetRemoteWindowCoords() {
@@ -2219,8 +2293,10 @@ class QualityMonitorModel with ChangeNotifier {
updateQualityStatus(Map<String, dynamic> evt) { updateQualityStatus(Map<String, dynamic> evt) {
try { try {
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) {
if ((evt['fps'] as String).isNotEmpty) { _data.speed = evt['speed'];
}
if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) {
final fps = jsonDecode(evt['fps']) as Map<String, dynamic>; final fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
final pi = parent.target?.ffiModel.pi; final pi = parent.target?.ffiModel.pi;
if (pi != null) { if (pi != null) {
@@ -2241,14 +2317,18 @@ class QualityMonitorModel with ChangeNotifier {
_data.fps = null; _data.fps = null;
} }
} }
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) {
if ((evt['target_bitrate'] as String).isNotEmpty) { _data.delay = evt['delay'];
}
if (evt.containsKey('target_bitrate') &&
(evt['target_bitrate'] as String).isNotEmpty) {
_data.targetBitrate = evt['target_bitrate']; _data.targetBitrate = evt['target_bitrate'];
} }
if ((evt['codec_format'] as String).isNotEmpty) { if (evt.containsKey('codec_format') &&
(evt['codec_format'] as String).isNotEmpty) {
_data.codecFormat = evt['codec_format']; _data.codecFormat = evt['codec_format'];
} }
if ((evt['chroma'] as String).isNotEmpty) { if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) {
_data.chroma = evt['chroma']; _data.chroma = evt['chroma'];
} }
notifyListeners(); notifyListeners();
@@ -2378,6 +2458,9 @@ class FFI {
late final ElevationModel elevationModel; // session late final ElevationModel elevationModel; // session
late final CmFileModel cmFileModel; // cm late final CmFileModel cmFileModel; // cm
late final TextureModel textureModel; //session late final TextureModel textureModel; //session
late final Peers recentPeersModel; // global
late final Peers favoritePeersModel; // global
late final Peers lanPeersModel; // global
FFI(SessionID? sId) { FFI(SessionID? sId) {
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
@@ -2398,6 +2481,16 @@ class FFI {
elevationModel = ElevationModel(WeakReference(this)); elevationModel = ElevationModel(WeakReference(this));
cmFileModel = CmFileModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this));
textureModel = TextureModel(WeakReference(this)); textureModel = TextureModel(WeakReference(this));
recentPeersModel = Peers(
name: PeersModelName.recent,
loadEvent: LoadEvent.recent,
getInitPeers: null);
favoritePeersModel = Peers(
name: PeersModelName.favorite,
loadEvent: LoadEvent.favorite,
getInitPeers: null);
lanPeersModel = Peers(
name: PeersModelName.lan, loadEvent: LoadEvent.lan, getInitPeers: null);
} }
/// Mobile reuse FFI /// Mobile reuse FFI
@@ -2492,6 +2585,7 @@ class FFI {
onEvent2UIRgba(); onEvent2UIRgba();
imageModel.onRgba(display, data); imageModel.onRgba(display, data);
}); });
this.id = id;
return; return;
} }
@@ -2547,32 +2641,30 @@ class FFI {
} }
} else if (message is EventToUI_Rgba) { } else if (message is EventToUI_Rgba) {
final display = message.field0; final display = message.field0;
if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) { // Fetch the image buffer from rust codes.
//debugPrint("EventToUI_Rgba display:$display"); final sz = platformFFI.getRgbaSize(sessionId, display);
textureModel.setTextureType(display: display, gpuTexture: false); if (sz == 0) {
platformFFI.nextRgba(sessionId, display);
return;
}
final rgba = platformFFI.getRgba(sessionId, display, sz);
if (rgba != null) {
onEvent2UIRgba(); onEvent2UIRgba();
await imageModel.onRgba(display, rgba);
} else { } else {
// Fetch the image buffer from rust codes. platformFFI.nextRgba(sessionId, display);
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);
}
} }
} else if (message is EventToUI_Texture) { } else if (message is EventToUI_Texture) {
final display = message.field0; final display = message.field0;
debugPrint("EventToUI_Texture display:$display"); final gpuTexture = message.field1;
if (hasGpuTextureRender) { debugPrint(
textureModel.setTextureType(display: display, gpuTexture: true); "EventToUI_Texture display:$display, gpuTexture:$gpuTexture");
onEvent2UIRgba(); if (gpuTexture && !hasGpuTextureRender) {
debugPrint('the gpuTexture is not supported.');
return;
} }
textureModel.setTextureType(display: display, gpuTexture: gpuTexture);
onEvent2UIRgba();
} }
}(); }();
}); });
@@ -2608,8 +2700,9 @@ class FFI {
remember: remember); remember: remember);
} }
void send2FA(SessionID sessionId, String code) { void send2FA(SessionID sessionId, String code, bool trustThisDevice) {
bind.sessionSend2Fa(sessionId: sessionId, code: code); bind.sessionSend2Fa(
sessionId: sessionId, code: code, trustThisDevice: trustThisDevice);
} }
/// Close the remote session. /// Close the remote session.
@@ -2626,7 +2719,7 @@ class FFI {
canvasModel.scale, canvasModel.scale,
ffiModel.pi.currentDisplay); ffiModel.pi.currentDisplay);
} }
imageModel.update(null); await imageModel.update(null);
cursorModel.clear(); cursorModel.clear();
ffiModel.clear(); ffiModel.clear();
canvasModel.clear(); canvasModel.clear();

View File

@@ -48,6 +48,12 @@ class PlatformFFI {
static get isMain => instance._appType == kAppTypeMain; static get isMain => instance._appType == kAppTypeMain;
static String getByName(String name, [String arg = '']) {
return '';
}
static void setByName(String name, [String value = '']) {}
static Future<String> getVersion() async { static Future<String> getVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version; return packageInfo.version;
@@ -276,4 +282,6 @@ class PlatformFFI {
void syncAndroidServiceAppDirConfigPath() { void syncAndroidServiceAppDirConfigPath() {
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir); invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
} }
void setFullscreenCallback(void Function(bool) fun) {}
} }

View File

@@ -194,10 +194,14 @@ class Peers extends ChangeNotifier {
} }
void _updateOnlineState(Map<String, dynamic> evt) { void _updateOnlineState(Map<String, dynamic> evt) {
int changedCount = 0;
evt['onlines'].split(',').forEach((online) { evt['onlines'].split(',').forEach((online) {
for (var i = 0; i < peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (peers[i].id == online) { if (peers[i].id == online) {
peers[i].online = true; if (!peers[i].online) {
changedCount += 1;
peers[i].online = true;
}
} }
} }
}); });
@@ -205,13 +209,18 @@ class Peers extends ChangeNotifier {
evt['offlines'].split(',').forEach((offline) { evt['offlines'].split(',').forEach((offline) {
for (var i = 0; i < peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (peers[i].id == offline) { if (peers[i].id == offline) {
peers[i].online = false; if (peers[i].online) {
changedCount += 1;
peers[i].online = false;
}
} }
} }
}); });
event = UpdateEvent.online; if (changedCount > 0) {
notifyListeners(); event = UpdateEvent.online;
notifyListeners();
}
} }
void _updatePeers(Map<String, dynamic> evt) { void _updatePeers(Map<String, dynamic> evt) {

View File

@@ -152,7 +152,7 @@ class PeerTabModel with ChangeNotifier {
// https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700 // https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700
// After onTap, the shift key should be pressed for a while when not in multiselection mode, // After onTap, the shift key should be pressed for a while when not in multiselection mode,
// because onTap is delayed when onDoubleTap is not null // because onTap is delayed when onDoubleTap is not null
if (isDesktop && !_isShiftDown) return; if (isDesktop || isWebDesktop) return;
_multiSelectionMode = true; _multiSelectionMode = true;
} }
final cached = _currentTabCachedPeers.map((e) => e.id).toList(); final cached = _currentTabCachedPeers.map((e) => e.id).toList();
@@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// `notifyListeners()` will cause many rebuilds.
// So, we need to reduce the calls to "notifyListeners()" only when necessary.
// A better way is to use a new model.
setCurrentTabCachedPeers(List<Peer> peers) { setCurrentTabCachedPeers(List<Peer> peers) {
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
final isPreEmpty = _currentTabCachedPeers.isEmpty;
_currentTabCachedPeers = peers; _currentTabCachedPeers = peers;
notifyListeners(); final isNowEmpty = _currentTabCachedPeers.isEmpty;
if (isPreEmpty != isNowEmpty) {
notifyListeners();
}
}); });
} }

View File

@@ -6,3 +6,11 @@ final platformFFI = PlatformFFI.instance;
final localeName = PlatformFFI.localeName; final localeName = PlatformFFI.localeName;
RustdeskImpl get bind => platformFFI.ffiBind; RustdeskImpl get bind => platformFFI.ffiBind;
String ffiGetByName(String name, [String arg = '']) {
return PlatformFFI.getByName(name, arg);
}
void ffiSetByName(String name, [String value = '']) {
PlatformFFI.setByName(name, value);
}

View File

@@ -196,7 +196,7 @@ class ServerModel with ChangeNotifier {
bind.mainSetOption(key: kOptionEnableAudio, value: "N"); bind.mainSetOption(key: kOptionEnableAudio, value: "N");
} else { } else {
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio); final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
_audioOk = audioOption.isEmpty; _audioOk = audioOption != 'N';
} }
// file // file
@@ -206,7 +206,7 @@ class ServerModel with ChangeNotifier {
} else { } else {
final fileOption = final fileOption =
await bind.mainGetOption(key: kOptionEnableFileTransfer); await bind.mainGetOption(key: kOptionEnableFileTransfer);
_fileOk = fileOption.isEmpty; _fileOk = fileOption != 'N';
} }
notifyListeners(); notifyListeners();
@@ -826,7 +826,7 @@ class Client {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id; data['id'] = id;
data['is_start'] = authorized; data['authorized'] = authorized;
data['is_file_transfer'] = isFileTransfer; data['is_file_transfer'] = isFileTransfer;
data['port_forward'] = portForward; data['port_forward'] = portForward;
data['name'] = name; data['name'] = name;
@@ -840,6 +840,8 @@ class Client {
data['block_input'] = blockInput; data['block_input'] = blockInput;
data['disconnected'] = disconnected; data['disconnected'] = disconnected;
data['from_switch'] = fromSwitch; data['from_switch'] = fromSwitch;
data['in_voice_call'] = inVoiceCall;
data['incoming_voice_call'] = incomingVoiceCall;
return data; return data;
} }

View File

@@ -14,11 +14,16 @@ class StateGlobal {
bool _isMinimized = false; bool _isMinimized = false;
final RxBool isMaximized = false.obs; final RxBool isMaximized = false.obs;
final RxBool _showTabBar = true.obs; final RxBool _showTabBar = true.obs;
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize); final RxDouble _resizeEdgeSize = RxDouble(windowResizeEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
final RxBool showRemoteToolBar = false.obs; final RxBool showRemoteToolBar = false.obs;
final svcStatus = SvcStatus.notReady.obs; final svcStatus = SvcStatus.notReady.obs;
final RxBool isFocused = false.obs; final RxBool isFocused = false.obs;
// for mobile and web
bool isInMainPage = true;
bool isWebVisible = true;
final isPortrait = false.obs;
String _inputSource = ''; String _inputSource = '';
@@ -68,32 +73,45 @@ class StateGlobal {
if (_fullscreen.value != v) { if (_fullscreen.value != v) {
_fullscreen.value = v; _fullscreen.value = v;
_showTabBar.value = !_fullscreen.value; _showTabBar.value = !_fullscreen.value;
refreshResizeEdgeSize(); if (isWebDesktop) {
print( procFullscreenWeb();
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); } else {
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; procFullscreenNative(procWnd);
if (procWnd) {
final wc = WindowController.fromWindowId(windowId);
wc.setFullscreen(_fullscreen.isTrue).then((_) {
// https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
if (isWindows && !v) {
Future.delayed(Duration.zero, () async {
final frame = await wc.getFrame();
final newRect = Rect.fromLTWH(
frame.left, frame.top, frame.width + 1, frame.height + 1);
await wc.setFrame(newRect);
});
}
});
} }
} }
} }
procFullscreenWeb() {
final isFullscreen = ffiGetByName('fullscreen') == 'Y';
String fullscreenValue = '';
if (isFullscreen && _fullscreen.isFalse) {
fullscreenValue = 'N';
} else if (!isFullscreen && fullscreen.isTrue) {
fullscreenValue = 'Y';
}
if (fullscreenValue.isNotEmpty) {
ffiSetByName('fullscreen', fullscreenValue);
}
}
procFullscreenNative(bool procWnd) {
refreshResizeEdgeSize();
print("fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
if (procWnd) {
final wc = WindowController.fromWindowId(windowId);
wc.setFullscreen(_fullscreen.isTrue).then((_) {
// We remove the redraw (width + 1, height + 1), because this issue cannot be reproduced.
// https://github.com/rustdesk/rustdesk/issues/9675
});
}
}
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
? kFullScreenEdgeSize ? kFullScreenEdgeSize
: isMaximized.isTrue : isMaximized.isTrue
? kMaximizeEdgeSize ? kMaximizeEdgeSize
: windowEdgeSize; : windowResizeEdgeSize;
String getInputSource({bool force = false}) { String getInputSource({bool force = false}) {
if (force || _inputSource.isEmpty) { if (force || _inputSource.isEmpty) {
@@ -107,7 +125,13 @@ class StateGlobal {
_inputSource = bind.mainGetInputSource(); _inputSource = bind.mainGetInputSource();
} }
StateGlobal._(); StateGlobal._() {
if (isWebDesktop) {
platformFFI.setFullscreenCallback((v) {
_fullscreen.value = v;
});
}
}
static final StateGlobal instance = StateGlobal._(); static final StateGlobal instance = StateGlobal._();
} }

View File

@@ -17,13 +17,23 @@ bool refreshingUser = false;
class UserModel { class UserModel {
final RxString userName = ''.obs; final RxString userName = ''.obs;
final RxBool isAdmin = false.obs; final RxBool isAdmin = false.obs;
final RxString networkError = ''.obs;
bool get isLogin => userName.isNotEmpty; bool get isLogin => userName.isNotEmpty;
WeakReference<FFI> parent; WeakReference<FFI> parent;
UserModel(this.parent); UserModel(this.parent) {
userName.listen((p0) {
// When user name becomes empty, show login button
// When user name becomes non-empty:
// For _updateLocalUserInfo, network error will be set later
// For login success, should clear network error
networkError.value = '';
});
}
void refreshCurrentUser() async { void refreshCurrentUser() async {
if (bind.isDisableAccount()) return; if (bind.isDisableAccount()) return;
networkError.value = '';
final token = bind.mainGetLocalOption(key: 'access_token'); final token = bind.mainGetLocalOption(key: 'access_token');
if (token == '') { if (token == '') {
await updateOtherModels(); await updateOtherModels();
@@ -38,12 +48,18 @@ class UserModel {
if (refreshingUser) return; if (refreshingUser) return;
try { try {
refreshingUser = true; refreshingUser = true;
final response = await http.post(Uri.parse('$url/api/currentUser'), final http.Response response;
headers: { try {
'Content-Type': 'application/json', response = await http.post(Uri.parse('$url/api/currentUser'),
'Authorization': 'Bearer $token' headers: {
}, 'Content-Type': 'application/json',
body: json.encode(body)); 'Authorization': 'Bearer $token'
},
body: json.encode(body));
} catch (e) {
networkError.value = e.toString();
rethrow;
}
refreshingUser = false; refreshingUser = false;
final status = response.statusCode; final status = response.statusCode;
if (status == 401 || status == 400) { if (status == 401 || status == 400) {

View File

@@ -1,12 +1,14 @@
// ignore_for_file: avoid_web_libraries_in_flutter // ignore_for_file: avoid_web_libraries_in_flutter
import 'dart:convert'; import 'dart:convert';
import 'dart:js_interop';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:js'; import 'dart:js';
import 'dart:html'; import 'dart:html';
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/web/bridge.dart'; import 'package:flutter_hbb/web/bridge.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
@@ -28,7 +30,15 @@ class PlatformFFI {
context.callMethod('setByName', [name, value]); context.callMethod('setByName', [name, value]);
} }
PlatformFFI._(); PlatformFFI._() {
window.document.addEventListener(
'visibilitychange',
(event) => {
stateGlobal.isWebVisible =
window.document.visibilityState == 'visible'
});
}
static final PlatformFFI instance = PlatformFFI._(); static final PlatformFFI instance = PlatformFFI._();
static get localeName => window.navigator.language; static get localeName => window.navigator.language;
@@ -98,6 +108,10 @@ class PlatformFFI {
sessionId: sessionId, display: display, ptr: ptr); sessionId: sessionId, display: display, ptr: ptr);
Future<void> init(String appType) async { Future<void> init(String appType) async {
Completer completer = Completer();
context["onInitFinished"] = () {
completer.complete();
};
context.callMethod('init'); context.callMethod('init');
version = getByName('version'); version = getByName('version');
window.onContextMenu.listen((event) { window.onContextMenu.listen((event) {
@@ -112,6 +126,7 @@ class PlatformFFI {
print('json.decode fail(): $e'); print('json.decode fail(): $e');
} }
}; };
return completer.future;
} }
void setEventCallback(void Function(Map<String, dynamic>) fun) { void setEventCallback(void Function(Map<String, dynamic>) fun) {
@@ -157,4 +172,10 @@ class PlatformFFI {
// just for compilation // just for compilation
void syncAndroidServiceAppDirConfigPath() {} void syncAndroidServiceAppDirConfigPath() {}
void setFullscreenCallback(void Function(bool) fun) {
context["onFullscreenChanged"] = (bool v) {
fun(v);
};
}
} }

View File

@@ -11,3 +11,7 @@ final isWebDesktop_ = false;
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux; final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
String get screenInfo_ => ''; String get screenInfo_ => '';
final isWebOnWindows_ = false;
final isWebOnLinux_ = false;
final isWebOnMacOS_ = false;

View File

@@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/model.dart';
deleteCustomCursor(String key) => deleteCustomCursor(String key) =>
custom_cursor_manager.CursorManager.instance.deleteCursor(key); custom_cursor_manager.CursorManager.instance.deleteCursor(key);
resetSystemCursor() {}
MouseCursor buildCursorOfCache( MouseCursor buildCursorOfCache(
CursorModel cursor, double scale, CursorData? cache) { CursorModel cursor, double scale, CursorData? cache) {

View File

@@ -13,14 +13,12 @@ Future<ui.Image?> decodeImageFromPixels(
int? rowBytes, int? rowBytes,
int? targetWidth, int? targetWidth,
int? targetHeight, int? targetHeight,
VoidCallback? onPixelsCopied, // must ensure onPixelsCopied is called no matter this function succeeds
bool allowUpscaling = true, bool allowUpscaling = true,
}) async { }) async {
if (targetWidth != null) { if (targetWidth != null) {
assert(allowUpscaling || targetWidth <= width); assert(allowUpscaling || targetWidth <= width);
if (!(allowUpscaling || targetWidth <= width)) { if (!(allowUpscaling || targetWidth <= width)) {
print("not allow upscaling but targetWidth > width"); print("not allow upscaling but targetWidth > width");
onPixelsCopied?.call();
return null; return null;
} }
} }
@@ -28,7 +26,6 @@ Future<ui.Image?> decodeImageFromPixels(
assert(allowUpscaling || targetHeight <= height); assert(allowUpscaling || targetHeight <= height);
if (!(allowUpscaling || targetHeight <= height)) { if (!(allowUpscaling || targetHeight <= height)) {
print("not allow upscaling but targetHeight > height"); print("not allow upscaling but targetHeight > height");
onPixelsCopied?.call();
return null; return null;
} }
} }
@@ -36,9 +33,7 @@ Future<ui.Image?> decodeImageFromPixels(
final ui.ImmutableBuffer buffer; final ui.ImmutableBuffer buffer;
try { try {
buffer = await ui.ImmutableBuffer.fromUint8List(pixels); buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
onPixelsCopied?.call();
} catch (e) { } catch (e) {
onPixelsCopied?.call();
return null; return null;
} }

View File

@@ -124,6 +124,9 @@ class RustDeskMultiWindowManager {
bool withScreenRect, bool withScreenRect,
) async { ) async {
final windowController = await DesktopMultiWindow.createWindow(msg); final windowController = await DesktopMultiWindow.createWindow(msg);
if (isWindows) {
windowController.setInitBackgroundColor(Colors.black);
}
final windowId = windowController.windowId; final windowId = windowController.windowId;
if (!withScreenRect) { if (!withScreenRect) {
windowController windowController

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,7 @@
import 'dart:js' as js; import 'dart:js' as js;
import 'dart:html' as html;
// cycle imports, maybe we can improve this
import 'package:flutter_hbb/consts.dart';
final isAndroid_ = false; final isAndroid_ = false;
final isIOS_ = false; final isIOS_ = false;
@@ -11,3 +14,8 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
final isDesktop_ = false; final isDesktop_ = false;
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']); String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']);
final _localOs = js.context.callMethod('getByName', ['local_os', '']);
final isWebOnWindows_ = _localOs == kPeerPlatformWindows;
final isWebOnLinux_ = _localOs == kPeerPlatformLinux;
final isWebOnMacOS_ = _localOs == kPeerPlatformMacOS;

View File

@@ -58,6 +58,11 @@ class CursorManager {
]); ]);
} }
} }
Future<void> resetSystemCursor() async {
latestKey = '';
js.context.callMethod('setByName', ['cursor', 'auto']);
}
} }
class FlutterCustomMemoryImageCursor extends MouseCursor { class FlutterCustomMemoryImageCursor extends MouseCursor {
@@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession {
} }
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key); deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
resetSystemCursor() => CursorManager.instance.resetSystemCursor();
MouseCursor buildCursorOfCache( MouseCursor buildCursorOfCache(
model.CursorModel cursor, double scale, model.CursorData? cache) { model.CursorModel cursor, double scale, model.CursorData? cache) {

View File

@@ -0,0 +1,14 @@
Future<void> webselectFiles({required bool is_folder}) async {
throw UnimplementedError("webselectFiles");
}
Future<void> webSendLocalFiles(
{required int handleIndex,
required int actId,
required String path,
required String to,
required int fileNum,
required bool includeHidden,
required bool isRemote}) {
throw UnimplementedError("webSendLocalFiles");
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
class WebSettingsPage extends StatelessWidget {
const WebSettingsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return _buildDesktopButton(context);
}
Widget _buildDesktopButton(BuildContext context) {
return IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
DesktopSettingPage(initialTabkey: SettingsTabKey.general),
),
);
},
);
}
}

View File

@@ -6,7 +6,7 @@ class TextureRgbaRenderer {
} }
Future<bool> closeTexture(int key) { Future<bool> closeTexture(int key) {
throw UnimplementedError(); return Future(() => true);
} }
Future<bool> onRgba( Future<bool> onRgba(

View File

@@ -0,0 +1,30 @@
import 'dart:async';
import 'dart:convert';
import 'dart:js' as js;
Future<void> webselectFiles({required bool is_folder}) async {
return Future(
() => js.context.callMethod('setByName', ['select_files', is_folder]));
}
Future<void> webSendLocalFiles(
{required int handleIndex,
required int actId,
required String path,
required String to,
required int fileNum,
required bool includeHidden,
required bool isRemote}) {
return Future(() => js.context.callMethod('setByName', [
'send_local_files',
jsonEncode({
'id': actId,
'handle_index': handleIndex,
'path': path,
'to': to,
'file_num': fileNum,
'include_hidden': includeHidden,
'is_remote': isRemote,
})
]));
}

View File

@@ -95,17 +95,17 @@ SPEC CHECKSUMS:
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195

View File

@@ -45,5 +45,7 @@
<string>Record the sound from microphone for the purpose of the remote desktop.</string> <string>Record the sound from microphone for the purpose of the remote desktop.</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>LSUIElement</key>
<string>1</string>
</dict> </dict>
</plist> </plist>

2
flutter/ndk_x86.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter

View File

@@ -335,7 +335,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "336308d86ec8b9640504a371b50ba500eb779363" resolved-ref: "519350f1f40746798299e94786197d058353bac9"
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
source: git source: git
version: "0.1.0" version: "0.1.0"
@@ -377,9 +377,25 @@ packages:
path: "." path: "."
ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9" ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9" resolved-ref: "24cb88413fa5181d949ddacbb30a65d5c459e7d9"
url: "https://github.com/21pages/dynamic_layouts.git" url: "https://github.com/rustdesk-org/dynamic_layouts.git"
source: git source: git
version: "0.0.1+1" version: "0.0.1+1"
extended_text:
dependency: "direct main"
description:
name: extended_text
sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
extended_text_library:
dependency: transitive
description:
name: extended_text_library
sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829"
url: "https://pub.dev"
source: hosted
version: "12.0.0"
external_path: external_path:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -509,9 +525,9 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "38951317afe79d953ab25733667bd96e172a80d3" ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3" resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
url: "https://github.com/21pages/flutter_gpu_texture_renderer" url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
source: git source: git
version: "0.0.1" version: "0.0.1"
flutter_improved_scrolling: flutter_improved_scrolling:
@@ -1613,5 +1629,5 @@ packages:
source: hosted source: hosted
version: "0.2.1" version: "0.2.1"
sdks: sdks:
dart: ">=3.2.0 <4.0.0" dart: ">=3.3.0 <4.0.0"
flutter: ">=3.16.0" flutter: ">=3.19.0"

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
version: 1.2.7+46 version: 1.3.2+51
environment: environment:
sdk: '^3.1.0' sdk: '^3.1.0'
@@ -92,18 +92,19 @@ dependencies:
dropdown_button2: ^2.0.0 dropdown_button2: ^2.0.0
flutter_gpu_texture_renderer: flutter_gpu_texture_renderer:
git: git:
url: https://github.com/21pages/flutter_gpu_texture_renderer url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
ref: 38951317afe79d953ab25733667bd96e172a80d3 ref: 2ded7f146437a761ffe6981e2f742038f85ca68d
uuid: ^3.0.7 uuid: ^3.0.7
auto_size_text_field: ^2.2.1 auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0 flex_color_picker: ^3.3.0
dynamic_layouts: dynamic_layouts:
git: git:
url: https://github.com/21pages/dynamic_layouts.git url: https://github.com/rustdesk-org/dynamic_layouts.git
ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9 ref: 24cb88413fa5181d949ddacbb30a65d5c459e7d9
pull_down_button: ^0.9.3 pull_down_button: ^0.9.3
device_info_plus: ^9.1.0 device_info_plus: ^9.1.0
qr_flutter: ^4.1.0 qr_flutter: ^4.1.0
extended_text: 13.0.0
dev_dependencies: dev_dependencies:
icons_launcher: ^2.0.4 icons_launcher: ^2.0.4

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

View File

@@ -5,7 +5,7 @@ use std::{
}; };
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
use hbb_common::{allow_err, log}; use hbb_common::{allow_err, bail};
use hbb_common::{ use hbb_common::{
lazy_static, lazy_static,
tokio::sync::{ tokio::sync::{
@@ -25,6 +25,8 @@ pub use context_send::*;
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001; const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
#[cfg(target_os = "windows")]
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
pub(crate) use platform::create_cliprdr_context; pub(crate) use platform::create_cliprdr_context;
@@ -130,7 +132,7 @@ impl ClipboardFile {
) )
} }
pub fn is_stopping_allowed_from_peer(&self) -> bool { pub fn is_beginning_message(&self) -> bool {
matches!( matches!(
self, self,
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
@@ -198,7 +200,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
#[inline] #[inline]
fn send_data(conn_id: i32, data: ClipboardFile) { fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
return send_data_to_channel(conn_id, data); return send_data_to_channel(conn_id, data);
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@@ -210,25 +212,28 @@ fn send_data(conn_id: i32, data: ClipboardFile) {
} }
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
#[inline] #[inline]
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) { fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
// no need to handle result here
if let Some(msg_channel) = VEC_MSG_CHANNEL if let Some(msg_channel) = VEC_MSG_CHANNEL
.read() .read()
.unwrap() .unwrap()
.iter() .iter()
.find(|x| x.conn_id == conn_id) .find(|x| x.conn_id == conn_id)
{ {
allow_err!(msg_channel.sender.send(data)); msg_channel.sender.send(data)?;
Ok(())
} else {
bail!("conn_id not found");
} }
} }
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
#[inline] #[inline]
fn send_data_to_all(data: ClipboardFile) { fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
// no need to handle result here // Need more tests to see if it's necessary to handle the error.
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
allow_err!(msg_channel.sender.send(data.clone())); allow_err!(msg_channel.sender.send(data.clone()));
} }
Ok(())
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,3 +1,4 @@
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::{CliprdrError, CliprdrServiceContext}; use crate::{CliprdrError, CliprdrServiceContext};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -63,8 +64,10 @@ pub fn create_cliprdr_context(
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
} }
#[cfg(any(target_os = "linux", target_os = "macos"))]
struct DummyCliprdrContext {} struct DummyCliprdrContext {}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl CliprdrServiceContext for DummyCliprdrContext { impl CliprdrServiceContext for DummyCliprdrContext {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
Ok(()) Ok(())

View File

@@ -7,7 +7,7 @@
use crate::{ use crate::{
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
}; };
use hbb_common::log; use hbb_common::log;
use std::{ use std::{
@@ -998,7 +998,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE)
} }
}; };
// no need to handle result here // no need to handle result here
send_data(conn_id as _, data); allow_err!(send_data(conn_id as _, data));
0 0
} }
@@ -1045,7 +1045,13 @@ extern "C" fn client_format_list(
.iter() .iter()
.for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone()))); .for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone())));
} else { } else {
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => {}
Err(e) => {
log::error!("failed to send format list: {:?}", e);
return ERR_CODE_SEND_MSG;
}
}
} }
0 0
@@ -1067,9 +1073,13 @@ extern "C" fn client_format_list_response(
msg_flags msg_flags
); );
let data = ClipboardFile::FormatListResponse { msg_flags }; let data = ClipboardFile::FormatListResponse { msg_flags };
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => 0,
0 Err(e) => {
log::error!("failed to send format list response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }
extern "C" fn client_format_data_request( extern "C" fn client_format_data_request(
@@ -1090,10 +1100,13 @@ extern "C" fn client_format_data_request(
conn_id, conn_id,
requested_format_id requested_format_id
); );
// no need to handle result here match send_data(conn_id, data) {
send_data(conn_id, data); Ok(_) => 0,
Err(e) => {
0 log::error!("failed to send format data request: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }
extern "C" fn client_format_data_response( extern "C" fn client_format_data_response(
@@ -1125,9 +1138,13 @@ extern "C" fn client_format_data_response(
msg_flags, msg_flags,
format_data, format_data,
}; };
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => 0,
0 Err(e) => {
log::error!("failed to send format data response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }
extern "C" fn client_file_contents_request( extern "C" fn client_file_contents_request(
@@ -1175,9 +1192,13 @@ extern "C" fn client_file_contents_request(
clip_data_id, clip_data_id,
}; };
log::debug!("client_file_contents_request called, data: {:?}", &data); log::debug!("client_file_contents_request called, data: {:?}", &data);
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => 0,
0 Err(e) => {
log::error!("failed to send file contents request: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }
extern "C" fn client_file_contents_response( extern "C" fn client_file_contents_response(
@@ -1213,7 +1234,11 @@ extern "C" fn client_file_contents_response(
msg_flags, msg_flags,
stream_id stream_id
); );
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => 0,
0 Err(e) => {
log::error!("failed to send file contents response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }

View File

@@ -220,7 +220,8 @@ struct wf_clipboard
HWND hwnd; HWND hwnd;
HANDLE hmem; HANDLE hmem;
HANDLE thread; HANDLE thread;
HANDLE response_data_event; HANDLE formatDataRespEvent;
BOOL formatDataRespReceived;
LPDATAOBJECT data_obj; LPDATAOBJECT data_obj;
HANDLE data_obj_mutex; HANDLE data_obj_mutex;
@@ -228,6 +229,7 @@ struct wf_clipboard
ULONG req_fsize; ULONG req_fsize;
char *req_fdata; char *req_fdata;
HANDLE req_fevent; HANDLE req_fevent;
BOOL req_f_received;
size_t nFiles; size_t nFiles;
size_t file_array_size; size_t file_array_size;
@@ -287,6 +289,9 @@ static BOOL try_open_clipboard(HWND hwnd)
static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid, static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid,
void **ppvObject) void **ppvObject)
{ {
if (ppvObject == NULL)
return E_INVALIDARG;
if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown)) if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown))
{ {
IStream_AddRef(This); IStream_AddRef(This);
@@ -362,6 +367,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
} }
*pcbRead = clipboard->req_fsize; *pcbRead = clipboard->req_fsize;
// Check overflow, can not be a real case
if ((instance->m_lOffset.QuadPart + clipboard->req_fsize) < instance->m_lOffset.QuadPart) {
// It's better to crash to release the explorer.exe
// This is a critical error, because the explorer is waiting for the data
// and the m_lOffset is wrong(overflowed)
return S_FALSE;
}
instance->m_lOffset.QuadPart += clipboard->req_fsize; instance->m_lOffset.QuadPart += clipboard->req_fsize;
if (clipboard->req_fsize < cb) if (clipboard->req_fsize < cb)
@@ -517,11 +529,17 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **pp
static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc) static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc)
{ {
IStream *iStream; IStream *iStream = NULL;
BOOL success = FALSE; BOOL success = FALSE;
BOOL isDir = FALSE; BOOL isDir = FALSE;
CliprdrStream *instance; CliprdrStream *instance = NULL;
wfClipboard *clipboard = (wfClipboard *)pData; wfClipboard *clipboard = (wfClipboard *)pData;
if (!(pData && dsc))
{
return NULL;
}
instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream)); instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream));
if (instance) if (instance)
@@ -874,14 +892,18 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This
static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count, static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count,
void *data) void *data)
{ {
CliprdrDataObject *instance; CliprdrDataObject *instance = NULL;
IDataObject *iDataObject; IDataObject *iDataObject = NULL;
instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject)); instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject));
if (!instance) if (!instance)
goto error; goto error;
instance->m_pFormatEtc = NULL;
instance->m_pStgMedium = NULL;
iDataObject = &instance->iDataObject; iDataObject = &instance->iDataObject;
iDataObject->lpVtbl = NULL;
iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl)); iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl));
if (!iDataObject->lpVtbl) if (!iDataObject->lpVtbl)
@@ -929,7 +951,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc
return instance; return instance;
error: error:
CliprdrDataObject_Delete(instance); if (iDataObject && iDataObject->lpVtbl)
{
free(iDataObject->lpVtbl);
}
if (instance)
{
if (instance->m_pFormatEtc)
{
free(instance->m_pFormatEtc);
}
if (instance->m_pStgMedium)
{
free(instance->m_pStgMedium);
}
CliprdrDataObject_Delete(instance);
}
return NULL; return NULL;
} }
@@ -1010,6 +1049,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT
REFIID riid, void **ppvObject) REFIID riid, void **ppvObject)
{ {
(void)This; (void)This;
if (!ppvObject)
return E_INVALIDARG;
if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown)) if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown))
{ {
@@ -1198,6 +1239,7 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
WCHAR *unicode_name; WCHAR *unicode_name;
#if !defined(UNICODE) #if !defined(UNICODE)
size_t size; size_t size;
int towchar_count;
#endif #endif
if (!clipboard || !format_name) if (!clipboard || !format_name)
@@ -1205,6 +1247,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
#if defined(UNICODE) #if defined(UNICODE)
unicode_name = _wcsdup(format_name); unicode_name = _wcsdup(format_name);
if (!unicode_name)
return 0;
#else #else
size = _tcslen(format_name); size = _tcslen(format_name);
unicode_name = calloc(size + 1, sizeof(WCHAR)); unicode_name = calloc(size + 1, sizeof(WCHAR));
@@ -1212,11 +1256,13 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
if (!unicode_name) if (!unicode_name)
return 0; return 0;
MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0);
#endif if (towchar_count <= 0 || towchar_count > size)
if (!unicode_name)
return 0; return 0;
towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size);
if (towchar_count <= 0)
return 0;
#endif
for (i = 0; i < clipboard->map_size; i++) for (i = 0; i < clipboard->map_size; i++)
{ {
@@ -1312,6 +1358,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
if (!clipboard) if (!clipboard)
return -1; return -1;
// to-do:
// Directly use the environment variable `TEMP` is not safe.
// But this function is not used for now.
if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) == if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) ==
0) 0)
return -1; return -1;
@@ -1444,7 +1493,37 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID)
return rc; return rc;
} }
UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, void **data) // Ensure the event is not signaled, and reset it if it is.
UINT try_reset_event(HANDLE event)
{
if (!event)
{
return ERROR_INTERNAL_ERROR;
}
DWORD result = WaitForSingleObject(event, 0);
if (result == WAIT_OBJECT_0)
{
if (!ResetEvent(event))
{
return GetLastError();
}
else
{
return ERROR_SUCCESS;
}
}
else if (result == WAIT_TIMEOUT)
{
return ERROR_SUCCESS;
}
else
{
return ERROR_INTERNAL_ERROR;
}
}
UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data)
{ {
UINT rc = ERROR_SUCCESS; UINT rc = ERROR_SUCCESS;
clipboard->context->IsStopped = FALSE; clipboard->context->IsStopped = FALSE;
@@ -1456,7 +1535,21 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis); DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis);
if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE) if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE)
{ {
continue; if ((*recvedFlag) == TRUE) {
// The data has been received, but the event is still not signaled.
// We just skip the rest of the waiting and reset the flag.
*recvedFlag = FALSE;
// Explicitly set the waitRes to WAIT_OBJECT_0, because we have received the data.
waitRes = WAIT_OBJECT_0;
} else {
// The data has not been received yet, we should continue to wait.
continue;
}
}
if (!ResetEvent(event))
{
// NOTE: critical error here, crash may be better
} }
if (clipboard->context->IsStopped == TRUE) if (clipboard->context->IsStopped == TRUE)
@@ -1470,12 +1563,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
return ERROR_INTERNAL_ERROR; return ERROR_INTERNAL_ERROR;
} }
if (!ResetEvent(event))
{
// NOTE: critical error here, crash may be better
rc = ERROR_INTERNAL_ERROR;
}
if ((*data) == NULL) if ((*data) == NULL)
{ {
rc = ERROR_INTERNAL_ERROR; rc = ERROR_INTERNAL_ERROR;
@@ -1519,6 +1606,13 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest) if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest)
return ERROR_INTERNAL_ERROR; return ERROR_INTERNAL_ERROR;
rc = try_reset_event(clipboard->formatDataRespEvent);
if (rc != ERROR_SUCCESS)
{
return rc;
}
clipboard->formatDataRespReceived = FALSE;
remoteFormatId = get_remote_format_id(clipboard, formatId); remoteFormatId = get_remote_format_id(clipboard, formatId);
formatDataRequest.connID = connID; formatDataRequest.connID = connID;
@@ -1530,7 +1624,7 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
return rc; return rc;
} }
wait_response_event(connID, clipboard, clipboard->response_data_event, &clipboard->hmem); return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
} }
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index, UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
@@ -1543,7 +1637,17 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest) if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
return ERROR_INTERNAL_ERROR; return ERROR_INTERNAL_ERROR;
rc = try_reset_event(clipboard->req_fevent);
if (rc != ERROR_SUCCESS)
{
return rc;
}
clipboard->req_f_received = FALSE;
fileContentsRequest.connID = connID; fileContentsRequest.connID = connID;
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
// But it is OK, because it is only used to check if the stream is the same in
// `wf_cliprdr_server_file_contents_request()` function.
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid; fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
fileContentsRequest.listIndex = index; fileContentsRequest.listIndex = index;
fileContentsRequest.dwFlags = flag; fileContentsRequest.dwFlags = flag;
@@ -1558,7 +1662,7 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
return rc; return rc;
} }
return wait_response_event(connID, clipboard, clipboard->req_fevent, (void **)&clipboard->req_fdata); return wait_response_event(connID, clipboard, clipboard->req_fevent, &clipboard->req_f_received, (void **)&clipboard->req_fdata);
} }
static UINT cliprdr_send_response_filecontents( static UINT cliprdr_send_response_filecontents(
@@ -1788,6 +1892,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM
break; break;
case WM_DESTROYCLIPBOARD: case WM_DESTROYCLIPBOARD:
// to-do: clear clipboard data?
case WM_ASKCBFORMATNAME: case WM_ASKCBFORMATNAME:
case WM_HSCROLLCLIPBOARD: case WM_HSCROLLCLIPBOARD:
case WM_PAINTCLIPBOARD: case WM_PAINTCLIPBOARD:
@@ -1904,7 +2009,7 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po
LONG positionHigh, DWORD nRequested, DWORD *puSize) LONG positionHigh, DWORD nRequested, DWORD *puSize)
{ {
BOOL res = FALSE; BOOL res = FALSE;
HANDLE hFile; HANDLE hFile = NULL;
DWORD nGet, rc; DWORD nGet, rc;
if (!file_name || !buffer || !puSize) if (!file_name || !buffer || !puSize)
@@ -1932,9 +2037,11 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po
res = TRUE; res = TRUE;
error: error:
if (hFile)
if (!CloseHandle(hFile)) {
res = FALSE; if (!CloseHandle(hFile))
res = FALSE;
}
if (res) if (res)
*puSize = nGet; *puSize = nGet;
@@ -1945,8 +2052,8 @@ error:
/* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */ /* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */
static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen) static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen)
{ {
HANDLE hFile; HANDLE hFile = NULL;
FILEDESCRIPTORW *fd; FILEDESCRIPTORW *fd = NULL;
fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW)); fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW));
if (!fd) if (!fd)
@@ -1975,7 +2082,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
} }
fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh); fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh);
wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen); if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0]))
{
// The file name is too long, which is not a normal case.
// So we just return NULL.
CloseHandle(hFile);
free(fd);
return NULL;
}
wcsncpy_s(fd->cFileName, sizeof(fd->cFileName) / sizeof(fd->cFileName[0]), file_name + pathLen, wcslen(file_name + pathLen) + 1);
CloseHandle(hFile); CloseHandle(hFile);
return fd; return fd;
@@ -2024,7 +2140,12 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
if (!clipboard->file_names[clipboard->nFiles]) if (!clipboard->file_names[clipboard->nFiles])
return FALSE; return FALSE;
wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name); // `MAX_PATH` is long enough for the file name.
// So we just return FALSE if the file name is too long, which is not a normal case.
if ((wcslen(full_file_name) + 1) > MAX_PATH)
return FALSE;
wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1);
/* add to descriptor array */ /* add to descriptor array */
clipboard->fileDescriptor[clipboard->nFiles] = clipboard->fileDescriptor[clipboard->nFiles] =
wf_cliprdr_get_file_descriptor(full_file_name, pathLen); wf_cliprdr_get_file_descriptor(full_file_name, pathLen);
@@ -2048,8 +2169,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
if (!clipboard || !Dir) if (!clipboard || !Dir)
return FALSE; return FALSE;
// StringCchCopy(DirSpec, MAX_PATH, Dir); if (wcslen(Dir) + 3 > MAX_PATH)
// StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); return FALSE;
StringCchCopyW(DirSpec, MAX_PATH, Dir); StringCchCopyW(DirSpec, MAX_PATH, Dir);
StringCchCatW(DirSpec, MAX_PATH, L"\\*"); StringCchCatW(DirSpec, MAX_PATH, L"\\*");
@@ -2078,9 +2199,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
{ {
WCHAR DirAdd[MAX_PATH]; WCHAR DirAdd[MAX_PATH];
// StringCchCopy(DirAdd, MAX_PATH, Dir); if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
// StringCchCat(DirAdd, MAX_PATH, _T("\\")); return FALSE;
// StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName);
StringCchCopyW(DirAdd, MAX_PATH, Dir); StringCchCopyW(DirAdd, MAX_PATH, Dir);
StringCchCatW(DirAdd, MAX_PATH, L"\\"); StringCchCatW(DirAdd, MAX_PATH, L"\\");
StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName); StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName);
@@ -2094,10 +2214,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
else else
{ {
WCHAR fileName[MAX_PATH]; WCHAR fileName[MAX_PATH];
// StringCchCopy(fileName, MAX_PATH, Dir); if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
// StringCchCat(fileName, MAX_PATH, _T("\\")); return FALSE;
// StringCchCat(fileName, MAX_PATH, FindFileData.cFileName);
StringCchCopyW(fileName, MAX_PATH, Dir); StringCchCopyW(fileName, MAX_PATH, Dir);
StringCchCatW(fileName, MAX_PATH, L"\\"); StringCchCatW(fileName, MAX_PATH, L"\\");
StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName); StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName);
@@ -2242,9 +2360,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
if (context->EnableFiles) if (context->EnableFiles)
{ {
UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32)); UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32));
*p_conn_id = formatList->connID; if (p_conn_id) {
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) *p_conn_id = formatList->connID;
rc = CHANNEL_RC_OK; if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
rc = CHANNEL_RC_OK;
}
} }
else else
{ {
@@ -2265,16 +2385,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
// SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL); // SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL);
FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS)); FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS));
format_ids->connID = formatList->connID; if (format_ids)
format_ids->size = (UINT32)clipboard->map_size;
format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
for (i = 0; i < format_ids->size; ++i)
{ {
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; format_ids->connID = formatList->connID;
} format_ids->size = (UINT32)clipboard->map_size;
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
{ if (format_ids->formats)
rc = CHANNEL_RC_OK; {
for (i = 0; i < format_ids->size; ++i)
{
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id;
}
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids))
{
rc = CHANNEL_RC_OK;
}
else
{
rc = ERROR_INTERNAL_ERROR;
}
}
else
{
rc = ERROR_INTERNAL_ERROR;
}
} }
else else
{ {
@@ -2469,17 +2603,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
p += len + 1, clipboard->nFiles++) p += len + 1, clipboard->nFiles++)
{ {
int cchWideChar; int cchWideChar;
WCHAR *wFileName;
cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0); cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0);
wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR)); wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR));
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); if (wFileName)
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); {
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar);
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar);
free(wFileName);
}
else
{
rc = ERROR_INTERNAL_ERROR;
GlobalUnlock(stg_medium.hGlobal);
ReleaseStgMedium(&stg_medium);
goto exit;
}
} }
} }
GlobalUnlock(stg_medium.hGlobal); GlobalUnlock(stg_medium.hGlobal);
ReleaseStgMedium(&stg_medium); ReleaseStgMedium(&stg_medium);
resp: resp:
// size will not overflow, because size type is size_t (unsigned __int64)
size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW); size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW);
groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size); groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size);
@@ -2519,10 +2664,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
globlemem = (char *)GlobalLock(hClipdata); globlemem = (char *)GlobalLock(hClipdata);
size = (int)GlobalSize(hClipdata); size = (int)GlobalSize(hClipdata);
buff = malloc(size); buff = malloc(size);
CopyMemory(buff, globlemem, size); if (buff)
{
CopyMemory(buff, globlemem, size);
rc = ERROR_SUCCESS;
}
else
{
rc = ERROR_INTERNAL_ERROR;
}
GlobalUnlock(hClipdata); GlobalUnlock(hClipdata);
CloseClipboard(); CloseClipboard();
rc = ERROR_SUCCESS;
} }
} }
else else
@@ -2545,7 +2697,7 @@ exit:
response.requestedFormatData = (BYTE *)buff; response.requestedFormatData = (BYTE *)buff;
if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response)) if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response))
{ {
// CAUTION: if failed to send, server will wait a long time // CAUTION: if failed to send, server will wait a long time, default 30 seconds.
} }
if (buff) if (buff)
@@ -2621,9 +2773,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
rc = CHANNEL_RC_OK; rc = CHANNEL_RC_OK;
} while (0); } while (0);
if (!SetEvent(clipboard->response_data_event)) if (!SetEvent(clipboard->formatDataRespEvent))
{ {
// CAUTION: critical error here, process will hang up until wait timeout default 3min. // If failed to set event, set flag to indicate the event is received.
DEBUG_CLIPRDR("wf_cliprdr_server_format_data_response(), SetEvent failed with 0x%x", GetLastError());
clipboard->formatDataRespReceived = TRUE;
rc = ERROR_INTERNAL_ERROR; rc = ERROR_INTERNAL_ERROR;
} }
return rc; return rc;
@@ -2899,7 +3053,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
if (!SetEvent(clipboard->req_fevent)) if (!SetEvent(clipboard->req_fevent))
{ {
// CAUTION: critical error here, process will hang up until wait timeout default 3min. // If failed to set event, set flag to indicate the event is received.
DEBUG_CLIPRDR("wf_cliprdr_server_file_contents_response(), SetEvent failed with 0x%x", GetLastError());
clipboard->req_f_received = TRUE;
} }
return rc; return rc;
} }
@@ -2934,14 +3090,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
(formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping)))) (formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping))))
goto error; goto error;
if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL))) if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL)))
goto error; goto error;
clipboard->formatDataRespReceived = FALSE;
if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex"))) if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex")))
goto error; goto error;
if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL))) if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL)))
goto error; goto error;
clipboard->req_f_received = FALSE;
if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL))) if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL)))
goto error; goto error;
@@ -3002,8 +3160,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
clipboard->data_obj = NULL; clipboard->data_obj = NULL;
} }
if (clipboard->response_data_event) if (clipboard->formatDataRespEvent)
CloseHandle(clipboard->response_data_event); CloseHandle(clipboard->formatDataRespEvent);
if (clipboard->data_obj_mutex) if (clipboard->data_obj_mutex)
CloseHandle(clipboard->data_obj_mutex); CloseHandle(clipboard->data_obj_mutex);

View File

@@ -23,7 +23,7 @@ serde = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true }
log = "0.4" log = "0.4"
rdev = { git = "https://github.com/rustdesk-org/rdev" } 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" } hbb_common = { path = "../hbb_common" }
[features] [features]

Some files were not shown because too many files have changed in this diff Show More