528 Commits

287 changed files with 17225 additions and 7516 deletions

View File

@@ -8,6 +8,7 @@ on:
env: env:
FLUTTER_VERSION: "3.16.9" FLUTTER_VERSION: "3.16.9"
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
jobs: jobs:
generate_bridge: generate_bridge:
@@ -37,10 +38,8 @@ jobs:
git \ git \
g++ \ g++ \
libclang-10-dev \ libclang-10-dev \
libclang-dev \
libgtk-3-dev \ libgtk-3-dev \
llvm-10-dev \ llvm-10-dev \
llvm-dev \
nasm \ nasm \
ninja-build \ ninja-build \
pkg-config \ pkg-config \
@@ -49,9 +48,9 @@ jobs:
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1 uses: dtolnay/rust-toolchain@v1
with: with:
toolchain: stable toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }} targets: ${{ matrix.job.target }}
components: '' components: "rustfmt"
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:

View File

@@ -1,61 +0,0 @@
name: Flutter Nightly MacOS Arm64 Build
on:
#schedule:
# schedule build every night
# - cron: "0/6 * * * *"
workflow_dispatch:
env:
CARGO_NDK_VERSION: "3.1.2"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.16.9"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
FLUTTER_ELINUX_COMMIT_ID: "c02bd16e1630f5bd690b85c5c2456ac1920e25af"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
VERSION: "1.2.4"
NDK_VERSION: "r26b"
#signing keys env variable checks
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}'
RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}'
API_SERVER: '${{ secrets.API_SERVER }}'
UPLOAD_ARTIFACT: true
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-for-macOS-arm64:
name: build-for-macOS-arm64
runs-on: [self-hosted, macOS, ARM64]
steps:
#- name: Import the codesign cert
# if: env.MACOS_P12_BASE64 != null
# uses: apple-actions/import-codesign-certs@v1
# continue-on-error: true
# with:
# p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
# p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
# keychain: rustdesk
#- name: Check sign and import sign key
# if: env.MACOS_P12_BASE64 != null
# run: |
# security default-keychain -s rustdesk.keychain
# security find-identity -v
- name: Run
shell: bash
run: |
cd /opt/build
#./update_mac_template.sh
#security default-keychain -s rustdesk.keychain
#security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
./agent.sh

View File

@@ -21,6 +21,9 @@ on:
- ".github/**" - ".github/**"
- "docs/**" - "docs/**"
- "README.md" - "README.md"
- "res/**"
- "appimage/**"
- "flatpak/**"
jobs: jobs:
# ensure_cargo_fmt: # ensure_cargo_fmt:
@@ -71,12 +74,12 @@ jobs:
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
# - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
# - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-apple-darwin , os: macos-10.15 }
# - { target: x86_64-pc-windows-gnu , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2022 }
# - { target: x86_64-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-msvc , os: windows-2022 }
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps: steps:
@@ -103,6 +106,7 @@ jobs:
gcc \ gcc \
git \ git \
g++ \ g++ \
libpam0g-dev \
libasound2-dev \ libasound2-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@ on:
- ".github/**" - ".github/**"
- "docs/**" - "docs/**"
- "README.md" - "README.md"
- "res/**"
- "appimage/**"
- "flatpak/**"
jobs: jobs:
run-ci: run-ci:

View File

@@ -15,24 +15,4 @@ jobs:
secrets: inherit secrets: inherit
with: with:
upload-artifact: true upload-artifact: true
upload-tag: ${{ env.GITHUB_REF_NAME }} upload-tag: ${{ github.ref_name }}
update-fdroid-version-file:
name: Publish RustDesk version file for F-Droid updater
runs-on: ubuntu-latest
steps:
- name: Generate RustDesk version file
run: |
UPSTREAM_VERNAME="$GITHUB_REF_NAME"
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr -d '.')"
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
shell: bash
- name: Publish RustDesk version file
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: "fdroid-version"
files: |
./rustdesk-version.txt

View File

@@ -1,84 +0,0 @@
name: Flutter Windows History Build
on: [workflow_dispatch]
env:
LLVM_VERSION: "10.0"
FLUTTER_VERSION: "3.16.9"
TAG_NAME: "tmp"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VERSION: "1.2.4"
jobs:
build-for-history-windows:
name: ${{ matrix.job.date }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
ref: ${{ matrix.job.ref }}
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v1
with:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.job.target }}
override: true
components: rustfmt
profile: minimal # minimal component installation (ie, no documentation)
- name: Install flutter rust bridge deps
run: |
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
shell: bash
- name: Build rustdesk
run: python3 .\build.py --portable --hwcodec --flutter
- name: Build self-extracted executable
shell: bash
run: |
pushd ./libs/portable
python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ matrix.job.date }}-${{ matrix.job.target }}.exe
- name: Publish Release
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./SignOutput/rustdesk-*.exe

216
.github/workflows/playground.yml vendored Normal file
View File

@@ -0,0 +1,216 @@
name: playground
on:
#schedule:
# schedule build every night
# - cron: "0/6 * * * *"
workflow_dispatch:
env:
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
CARGO_NDK_VERSION: "3.1.2"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.13.9"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.03.25
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
VERSION: "1.2.6"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
API_SERVER: "${{ secrets.API_SERVER }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-rustdesk-android:
name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
arch: aarch64,
target: aarch64-linux-android,
os: ubuntu-20.04,
openssl-arch: android-arm64,
ref: master, # latest
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
clang \
cmake \
curl \
gcc-multilib \
git \
g++ \
g++-multilib \
libappindicator3-dev \
libasound2-dev \
libc6-dev \
libclang-10-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libvdpau-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
llvm-10-dev \
nasm \
yasm \
ninja-build \
openjdk-11-jdk-headless \
pkg-config \
tree \
wget
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
- name: Install flutter rust bridge deps
run: |
git config --global core.longpaths true
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml
pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; popd;
pushd flutter ; flutter pub get ; popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/build_android_deps.sh arm64-v8a
;;
armv7-linux-androideabi)
./flutter/build_android_deps.sh armeabi-v7a
;;
esac
shell: bash
- name: Clone deps
shell: bash
run: |
pushd /opt
git clone https://github.com/rustdesk-org/rustdesk_thirdparty_lib.git --depth=1
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
# cp -rf /opt/rustdesk_thirdparty_lib/vcpkg/* /opt/artifacts/vcpkg/
ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/
- name: Build rustdesk lib
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
rustup target add ${{ matrix.job.target }}
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }}
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/ndk_arm64.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
;;
armv7-linux-androideabi)
./flutter/ndk_arm.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
;;
esac
- name: Build rustdesk
shell: bash
env:
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
run: |
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
# temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
case ${{ matrix.job.target }} in
aarch64-linux-android)
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
# build flutter
pushd flutter
flutter build apk --release --target-platform android-arm64 --split-per-abi
mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
;;
armv7-linux-androideabi)
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
# build flutter
pushd flutter
flutter build apk --release --target-platform android-arm --split-per-abi
mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
;;
esac
popd
mkdir -p signed-apk; pushd signed-apk
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
with:
releaseDirectory: ./signed-apk
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
alias: ${{ secrets.ANDROID_ALIAS }}
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "30.0.2"
- name: Publish signed apk package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
${{steps.sign-rustdesk.outputs.signedReleaseFile}}

View File

@@ -10,7 +10,7 @@ on:
description: 'Target' description: 'Target'
required: true required: true
type: string type: string
default: 'windows-2019' default: 'windows-2022'
configuration: configuration:
description: 'Configuration' description: 'Configuration'
required: true required: true

View File

@@ -4,9 +4,9 @@ on:
types: [released] types: [released]
jobs: jobs:
publish: publish:
runs-on: windows-latest # action can only be run on windows runs-on: ubuntu-latest
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@v1 - uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: RustDesk.RustDesk identifier: RustDesk.RustDesk
version: ${{ github.event.release.tag_name }} version: ${{ github.event.release.tag_name }}

4
.gitignore vendored
View File

@@ -51,4 +51,6 @@ lib/generated_bridge.dart
# build cache in examples # build cache in examples
examples/**/target/ examples/**/target/
# === # ===
vcpkg_installed vcpkg_installed
flutter/lib/generated_plugin_registrant.dart
libsciter.dylib

456
Cargo.lock generated
View File

@@ -115,17 +115,6 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "amf"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"bindgen 0.59.2",
"cc",
"gpu_common",
"log",
]
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@@ -311,9 +300,9 @@ dependencies = [
[[package]] [[package]]
name = "async-compression" name = "async-compression"
version = "0.4.5" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
dependencies = [ dependencies = [
"flate2", "flate2",
"futures-core", "futures-core",
@@ -528,6 +517,12 @@ version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.6.0" version = "1.6.0"
@@ -768,9 +763,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.5.0" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
dependencies = [ dependencies = [
"serde 1.0.190", "serde 1.0.190",
] ]
@@ -1106,7 +1101,7 @@ dependencies = [
[[package]] [[package]]
name = "confy" name = "confy"
version = "0.4.0-2" version = "0.4.0-2"
source = "git+https://github.com/open-trade/confy#7855cd3c32b1a60b44e5076ee8f6b4131da10350" source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc5611bb382479"
dependencies = [ dependencies = [
"directories-next", "directories-next",
"serde 1.0.190", "serde 1.0.190",
@@ -1836,9 +1831,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
@@ -2097,7 +2092,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [ dependencies = [
"spin 0.9.8", "spin",
] ]
[[package]] [[package]]
@@ -2649,36 +2644,6 @@ dependencies = [
"system-deps 6.1.2", "system-deps 6.1.2",
] ]
[[package]]
name = "gpu_common"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"bindgen 0.59.2",
"cc",
"log",
"serde 1.0.190",
"serde_derive",
"serde_json 1.0.107",
]
[[package]]
name = "gpucodec"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"amf",
"bindgen 0.59.2",
"cc",
"gpu_common",
"log",
"nv",
"serde 1.0.190",
"serde_derive",
"serde_json 1.0.107",
"vpl",
]
[[package]] [[package]]
name = "gstreamer" name = "gstreamer"
version = "0.16.7" version = "0.16.7"
@@ -2866,9 +2831,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.24" version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -2917,6 +2882,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"backtrace", "backtrace",
"base64 0.22.0",
"bytes", "bytes",
"chrono", "chrono",
"confy", "confy",
@@ -2928,6 +2894,7 @@ dependencies = [
"flexi_logger", "flexi_logger",
"futures", "futures",
"futures-util", "futures-util",
"httparse",
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
@@ -2936,19 +2903,24 @@ dependencies = [
"osascript", "osascript",
"protobuf", "protobuf",
"protobuf-codegen", "protobuf-codegen",
"quinn",
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
"rustls-pki-types",
"rustls-platform-verifier",
"serde 1.0.190", "serde 1.0.190",
"serde_derive", "serde_derive",
"serde_json 1.0.107", "serde_json 1.0.107",
"socket2 0.3.19", "socket2 0.3.19",
"sodiumoxide", "sodiumoxide",
"sysinfo", "sysinfo",
"thiserror",
"tokio", "tokio",
"tokio-socks", "tokio-native-tls",
"tokio-rustls 0.26.0",
"tokio-socks 0.5.2-1",
"tokio-util", "tokio-util",
"toml 0.7.8", "toml 0.7.8",
"url",
"uuid", "uuid",
"winapi 0.3.9", "winapi 0.3.9",
"zstd 0.13.0", "zstd 0.13.0",
@@ -3025,9 +2997,9 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -3036,9 +3008,9 @@ dependencies = [
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.5" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@@ -3065,8 +3037,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hwcodec" name = "hwcodec"
version = "0.2.0" version = "0.4.18"
source = "git+https://github.com/21pages/hwcodec?branch=stable#52e1da2aae86acec5f374bc065f5921945b55e7b" source = "git+https://github.com/21pages/hwcodec#b84d5bbefa949194d1fc51a5c7e9d7988e315ce6"
dependencies = [ dependencies = [
"bindgen 0.59.2", "bindgen 0.59.2",
"cc", "cc",
@@ -3078,9 +3050,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.27" version = "0.14.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@@ -3093,7 +3065,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa 1.0.9", "itoa 1.0.9",
"pin-project-lite", "pin-project-lite",
"socket2 0.4.10", "socket2 0.5.5",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -3111,7 +3083,7 @@ dependencies = [
"hyper", "hyper",
"rustls 0.21.10", "rustls 0.21.10",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls 0.24.1",
] ]
[[package]] [[package]]
@@ -3791,15 +3763,6 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "miow"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044"
dependencies = [
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mouce" name = "mouce"
version = "0.2.1" version = "0.2.1"
@@ -4134,17 +4097,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "nv"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"bindgen 0.59.2",
"cc",
"gpu_common",
"log",
]
[[package]] [[package]]
name = "objc" name = "objc"
version = "0.2.7" version = "0.2.7"
@@ -4375,7 +4327,7 @@ dependencies = [
[[package]] [[package]]
name = "pam" name = "pam"
version = "0.7.0" version = "0.7.0"
source = "git+https://github.com/fufesou/pam#10da2cbbabe32cbc9de22a66abe44738e7ec0ea0" source = "git+https://github.com/fufesou/pam#3a2aaa6e07b176d8e2d66a5eec38d2ddb45f009f"
dependencies = [ dependencies = [
"libc", "libc",
"pam-macros", "pam-macros",
@@ -4397,8 +4349,7 @@ dependencies = [
[[package]] [[package]]
name = "pam-sys" name = "pam-sys"
version = "1.0.0-alpha4" version = "1.0.0-alpha4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/fufesou/pam-sys?branch=fix/v1.0.0-alpha4_gnuc_va_list#3337c9bb9a9c68d7497ec8c93cad2368c26091b7"
checksum = "5e9dfd42858f6a6bb1081079fd9dc259ca3e2aaece6cb689fd36b1058046c969"
dependencies = [ dependencies = [
"bindgen 0.59.2", "bindgen 0.59.2",
"libc", "libc",
@@ -4431,13 +4382,12 @@ dependencies = [
[[package]] [[package]]
name = "parity-tokio-ipc" name = "parity-tokio-ipc"
version = "0.7.3-3" version = "0.7.3-4"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#f2d1fcf8fb002335d9a62bec308559d40698694d" source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#3623ec9ebef50c9b118e03b03df831008a4d1441"
dependencies = [ dependencies = [
"futures", "futures",
"libc", "libc",
"log", "log",
"miow",
"rand 0.8.5", "rand 0.8.5",
"tokio", "tokio",
"winapi 0.3.9", "winapi 0.3.9",
@@ -4616,7 +4566,7 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa" checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa"
dependencies = [ dependencies = [
"base64", "base64 0.21.5",
"indexmap 1.9.3", "indexmap 1.9.3",
"line-wrap", "line-wrap",
"quick-xml", "quick-xml",
@@ -4762,9 +4712,9 @@ dependencies = [
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "3.3.0" version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" checksum = "58678a64de2fced2bdec6bca052a6716a0efe692d6e3f53d1bda6a1def64cfc0"
dependencies = [ dependencies = [
"bytes", "bytes",
"once_cell", "once_cell",
@@ -4774,9 +4724,9 @@ dependencies = [
[[package]] [[package]]
name = "protobuf-codegen" name = "protobuf-codegen"
version = "3.3.0" version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78" checksum = "32777b0b3f6538d9d2e012b3fad85c7e4b9244b5958d04a6415f4333782b7a77"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"once_cell", "once_cell",
@@ -4789,9 +4739,9 @@ dependencies = [
[[package]] [[package]]
name = "protobuf-parse" name = "protobuf-parse"
version = "3.3.0" version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba" checksum = "96cb37955261126624a25b5e6bda40ae34cf3989d52a783087ca6091b29b5642"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indexmap 1.9.3", "indexmap 1.9.3",
@@ -4805,9 +4755,9 @@ dependencies = [
[[package]] [[package]]
name = "protobuf-support" name = "protobuf-support"
version = "3.3.0" version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" checksum = "e1ed294a835b0f30810e13616b1cd34943c6d1e84a8f3b0dcfe466d256c3e7e7"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
@@ -4860,56 +4810,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quinn"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e8b432585672228923edbbf64b8b12c14e1112f62e88737655b4a083dbcd78e"
dependencies = [
"bytes",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls 0.20.9",
"thiserror",
"tokio",
"tracing",
"webpki",
]
[[package]]
name = "quinn-proto"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b0b33c13a79f669c85defaf4c275dc86a0c0372807d0ca3d78e0bb87274863"
dependencies = [
"bytes",
"rand 0.8.5",
"ring 0.16.20",
"rustc-hash",
"rustls 0.20.9",
"rustls-native-certs",
"slab",
"thiserror",
"tinyvec",
"tracing",
"webpki",
]
[[package]]
name = "quinn-udp"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
dependencies = [
"libc",
"quinn-proto",
"socket2 0.4.10",
"tracing",
"windows-sys 0.42.0",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "0.6.13" version = "0.6.13"
@@ -5224,10 +5124,10 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.23" version = "0.11.23"
source = "git+https://github.com/rustdesk-org/reqwest" source = "git+https://github.com/rustdesk-org/reqwest#9cb758c9fb2f4edc62eb790acfd45a6a3da21ed3"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"base64", "base64 0.21.5",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -5247,8 +5147,8 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls 0.21.10", "rustls 0.21.10",
"rustls-native-certs", "rustls-native-certs 0.6.3",
"rustls-pemfile", "rustls-pemfile 1.0.3",
"serde 1.0.190", "serde 1.0.190",
"serde_json 1.0.107", "serde_json 1.0.107",
"serde_urlencoded", "serde_urlencoded",
@@ -5256,32 +5156,18 @@ dependencies = [
"system-configuration", "system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls", "tokio-rustls 0.24.1",
"tokio-socks 0.5.1",
"tokio-util", "tokio-util",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots 0.25.4",
"winreg 0.50.0", "winreg 0.50.0",
] ]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin 0.5.2",
"untrusted 0.7.1",
"web-sys",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.5" version = "0.17.5"
@@ -5291,8 +5177,8 @@ dependencies = [
"cc", "cc",
"getrandom", "getrandom",
"libc", "libc",
"spin 0.9.8", "spin",
"untrusted 0.9.0", "untrusted",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@@ -5402,14 +5288,13 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk" name = "rustdesk"
version = "1.2.4" version = "1.2.6"
dependencies = [ dependencies = [
"android-wakelock", "android-wakelock",
"android_logger", "android_logger",
"arboard", "arboard",
"async-process", "async-process",
"async-trait", "async-trait",
"base64",
"bytes", "bytes",
"cc", "cc",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@@ -5500,7 +5385,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.2.4" version = "1.2.6"
dependencies = [ dependencies = [
"brotli", "brotli",
"dirs 5.0.1", "dirs 5.0.1",
@@ -5551,17 +5436,6 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "rustls"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
dependencies = [
"ring 0.16.20",
"sct",
"webpki",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.21.10" version = "0.21.10"
@@ -5569,11 +5443,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
dependencies = [ dependencies = [
"log", "log",
"ring 0.17.5", "ring",
"rustls-webpki", "rustls-webpki 0.101.7",
"sct", "sct",
] ]
[[package]]
name = "rustls"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.2",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.6.3" version = "0.6.3"
@@ -5581,7 +5470,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pemfile", "rustls-pemfile 1.0.3",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.1.2",
"rustls-pki-types",
"schannel", "schannel",
"security-framework", "security-framework",
] ]
@@ -5592,17 +5494,71 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [ dependencies = [
"base64", "base64 0.21.5",
] ]
[[package]]
name = "rustls-pemfile"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [
"base64 0.22.0",
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
[[package]]
name = "rustls-platform-verifier"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5f0d26fa1ce3c790f9590868f0109289a044acb954525f933e2aa3b871c157d"
dependencies = [
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.8.4",
"jni 0.19.0",
"log",
"once_cell",
"rustls 0.23.4",
"rustls-native-certs 0.7.0",
"rustls-platform-verifier-android",
"rustls-webpki 0.102.2",
"security-framework",
"security-framework-sys",
"webpki-roots 0.26.1",
"winapi 0.3.9",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.101.7" version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [ dependencies = [
"ring 0.17.5", "ring",
"untrusted 0.9.0", "untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.102.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
] ]
[[package]] [[package]]
@@ -5683,7 +5639,6 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"dbus", "dbus",
"docopt", "docopt",
"gpucodec",
"gstreamer", "gstreamer",
"gstreamer-app", "gstreamer-app",
"gstreamer-video", "gstreamer-video",
@@ -5712,28 +5667,29 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [ dependencies = [
"ring 0.17.5", "ring",
"untrusted 0.9.0", "untrusted",
] ]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.9.2" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.8.4", "core-foundation-sys 0.8.4",
"libc", "libc",
"num-bigint",
"security-framework-sys", "security-framework-sys",
] ]
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.9.1" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [ dependencies = [
"core-foundation-sys 0.8.4", "core-foundation-sys 0.8.4",
"libc", "libc",
@@ -5971,12 +5927,6 @@ dependencies = [
"serde 1.0.190", "serde 1.0.190",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@@ -6177,7 +6127,7 @@ dependencies = [
[[package]] [[package]]
name = "tao" name = "tao"
version = "0.25.0" version = "0.25.0"
source = "git+https://github.com/rustdesk-org/tao?branch=dev#1cad16b200485bbccc67dcee2d339eac6e1c16ad" source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"cc", "cc",
@@ -6217,7 +6167,7 @@ dependencies = [
[[package]] [[package]]
name = "tao-macros" name = "tao-macros"
version = "0.1.2" version = "0.1.2"
source = "git+https://github.com/rustdesk-org/tao?branch=dev#1cad16b200485bbccc67dcee2d339eac6e1c16ad" source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3"
dependencies = [ dependencies = [
"proc-macro2 1.0.79", "proc-macro2 1.0.79",
"quote 1.0.35", "quote 1.0.35",
@@ -6408,9 +6358,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.36.0" version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -6427,9 +6377,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2 1.0.79", "proc-macro2 1.0.79",
"quote 1.0.35", "quote 1.0.35",
@@ -6456,10 +6406,33 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.4",
"rustls-pki-types",
"tokio",
]
[[package]] [[package]]
name = "tokio-socks" name = "tokio-socks"
version = "0.5.1-2" version = "0.5.1"
source = "git+https://github.com/open-trade/tokio-socks#14a5c2564fa20a2765ea53d03c573ee2b7e20421" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
dependencies = [
"either",
"futures-util",
"thiserror",
"tokio",
]
[[package]]
name = "tokio-socks"
version = "0.5.2-1"
source = "git+https://github.com/rustdesk-org/tokio-socks#94e97c6d7c93b0bcbfa54f2dc397c1da0a6e43d3"
dependencies = [ dependencies = [
"bytes", "bytes",
"either", "either",
@@ -6689,9 +6662,9 @@ dependencies = [
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.4" 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 = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "typenum" name = "typenum"
@@ -6782,12 +6755,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 = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@@ -6900,17 +6867,6 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "vpl"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"bindgen 0.59.2",
"cc",
"gpu_common",
"log",
]
[[package]] [[package]]
name = "waker-fn" name = "waker-fn"
version = "1.1.1" version = "1.1.1"
@@ -7133,20 +7089,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "webpki" name = "webpki-roots"
version = "0.22.4" version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
dependencies = [
"ring 0.17.5",
"untrusted 0.9.0",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.25.3" version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "weezl" name = "weezl"
@@ -7370,21 +7325,6 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.45.0" version = "0.45.0"
@@ -7919,6 +7859,12 @@ dependencies = [
"syn 2.0.55", "syn 2.0.55",
] ]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
[[package]] [[package]]
name = "zip" name = "zip"
version = "0.6.6" version = "0.6.6"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.2.4" version = "1.2.6"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"
@@ -19,19 +19,14 @@ path = "src/naming.rs"
[features] [features]
inline = [] inline = []
cli = [] cli = []
flutter_texture_render = []
appimage = []
flatpak = []
use_samplerate = ["samplerate"] use_samplerate = ["samplerate"]
use_rubato = ["rubato"] use_rubato = ["rubato"]
use_dasp = ["dasp"] use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"] flutter = ["flutter_rust_bridge"]
default = ["use_dasp"] default = ["use_dasp"]
hwcodec = ["scrap/hwcodec"] hwcodec = ["scrap/hwcodec"]
gpucodec = ["scrap/gpucodec"] vram = ["scrap/vram"]
mediacodec = ["scrap/mediacodec"] mediacodec = ["scrap/mediacodec"]
linux_headless = ["pam" ]
virtual_display_driver = ["virtual_display"]
plugin_framework = [] plugin_framework = []
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"] linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
unix-file-copy-paste = [ unix-file-copy-paste = [
@@ -65,7 +60,6 @@ samplerate = { version = "0.2", optional = true }
uuid = { version = "1.3", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
clap = "4.2" clap = "4.2"
rpassword = "7.2" rpassword = "7.2"
base64 = "0.21"
num_cpus = "1.15" num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] } bytes = { version = "1.4", features = ["serde"] }
default-net = "0.14" default-net = "0.14"
@@ -100,10 +94,23 @@ system_shutdown = "4.0"
qrcode-generator = "4.1" qrcode-generator = "4.1"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] } winapi = { version = "0.3", features = [
"winuser",
"wincrypt",
"shellscalingapi",
"pdh",
"synchapi",
"memoryapi",
"shellapi",
"devguid",
"setupapi",
"cguid",
"cfgmgr32",
"ioapiset",
] }
winreg = "0.11" winreg = "0.11"
windows-service = "0.6" windows-service = "0.6"
virtual_display = { path = "libs/virtual_display", optional = true } virtual_display = { path = "libs/virtual_display" }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" } impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
shared_memory = "0.12" shared_memory = "0.12"
tauri-winrt-notification = "0.1.2" tauri-winrt-notification = "0.1.2"
@@ -132,10 +139,10 @@ wallpaper = { git = "https://github.com/21pages/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
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false } reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] [target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false } reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" } psimple = { package = "libpulse-simple-binding", version = "2.27" }
@@ -146,7 +153,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
evdev = { git="https://github.com/fufesou/evdev" } evdev = { git="https://github.com/fufesou/evdev" }
dbus = "0.9" dbus = "0.9"
dbus-crossroads = "0.5" dbus-crossroads = "0.5"
pam = { git="https://github.com/fufesou/pam", optional = true } pam = { git="https://github.com/fufesou/pam" }
users = { version = "0.11" } users = { version = "0.11" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true} x11rb = {version = "0.12", features = ["all-extensions"], optional = true}

View File

@@ -18,6 +18,7 @@ RUN apt update -y && \
libxcb-shape0-dev \ libxcb-shape0-dev \
libxcb-xfixes0-dev \ libxcb-xfixes0-dev \
libasound2-dev \ libasound2-dev \
libpam0g-dev \
libpulse-dev \ libpulse-dev \
make \ make \
cmake \ cmake \

View File

@@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.4.deb - bsdtar -zxvf rustdesk.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.4 version: 1.2.6
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
@@ -26,18 +26,18 @@ AppDir:
- arm64 - arm64
allow_unauthenticated: true allow_unauthenticated: true
sources: sources:
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted
universe multiverse universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted
universe multiverse universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
include: include:
- libc6 - libc6:arm64
- libgtk-3-0 - libgtk-3-0
- libxcb-randr0 - libxcb-randr0
- libxdo3 - libxdo3
@@ -51,9 +51,15 @@ AppDir:
- libva-x11-2 - libva-x11-2
- libvdpau1 - libvdpau1
- libgstreamer-plugins-base1.0-0 - libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire
- libwayland-client0
- libwayland-cursor0 - libwayland-cursor0
- libwayland-egl1 - libwayland-egl1
- libpulse0 - libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude: exclude:
- humanity-icon-theme - humanity-icon-theme
- hicolor-icon-theme - hicolor-icon-theme
@@ -69,8 +75,11 @@ AppDir:
- usr/share/doc/*/TODO.* - usr/share/doc/*/TODO.*
runtime: runtime:
env: env:
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
GDK_BACKEND: x11 GDK_BACKEND: x11
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
test: test:
fedora-30: fedora-30:
image: appimagecrafters/tests-env:fedora-30 image: appimagecrafters/tests-env:fedora-30

View File

@@ -2,7 +2,7 @@
version: 1 version: 1
script: script:
- rm -rf ./AppDir || true - rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.4.deb - bsdtar -zxvf rustdesk.deb
- tar -xvf ./data.tar.xz - tar -xvf ./data.tar.xz
- mkdir ./AppDir - mkdir ./AppDir
- mv ./usr ./AppDir/usr - mv ./usr ./AppDir/usr
@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.2.4 version: 1.2.6
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:
@@ -26,18 +26,16 @@ AppDir:
- amd64 - amd64
allow_unauthenticated: true allow_unauthenticated: true
sources: sources:
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted
universe multiverse universe multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
universe multiverse universe multiverse
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
bionic main
include: include:
- libc6:amd64 - libc6:amd64
- libgtk-3-0 - libgtk-3-0
@@ -54,9 +52,14 @@ AppDir:
- libvdpau1 - libvdpau1
- libgstreamer-plugins-base1.0-0 - libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire - gstreamer1.0-pipewire
- libwayland-client0
- libwayland-cursor0 - libwayland-cursor0
- libwayland-egl1 - libwayland-egl1
- libpulse0 - libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude: exclude:
- humanity-icon-theme - humanity-icon-theme
- hicolor-icon-theme - hicolor-icon-theme
@@ -72,8 +75,11 @@ AppDir:
- usr/share/doc/*/TODO.* - usr/share/doc/*/TODO.*
runtime: runtime:
env: env:
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
GDK_BACKEND: x11 GDK_BACKEND: x11
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
test: test:
fedora-30: fedora-30:
image: appimagecrafters/tests-env:fedora-30 image: appimagecrafters/tests-env:fedora-30

View File

@@ -33,9 +33,9 @@ def get_arch() -> str:
def system2(cmd): def system2(cmd):
err = os.system(cmd) exit_code = os.system(cmd)
if err != 0: if exit_code != 0:
print(f"Error occurred when executing: {cmd}. Exiting.") sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n")
sys.exit(-1) sys.exit(-1)
@@ -118,9 +118,9 @@ def make_parser():
'' if windows or osx else ', need libva-dev, libvdpau-dev.') '' if windows or osx else ', need libva-dev, libvdpau-dev.')
) )
parser.add_argument( parser.add_argument(
'--gpucodec', '--vram',
action='store_true', action='store_true',
help='Enable feature gpucodec, only available on windows now.' help='Enable feature vram, only available on windows now.'
) )
parser.add_argument( parser.add_argument(
'--portable', '--portable',
@@ -132,16 +132,6 @@ def make_parser():
action='store_true', action='store_true',
help='Build with unix file copy paste feature' help='Build with unix file copy paste feature'
) )
parser.add_argument(
'--flatpak',
action='store_true',
help='Build rustdesk libs with the flatpak feature enabled'
)
parser.add_argument(
'--appimage',
action='store_true',
help='Build rustdesk libs with the appimage feature enabled'
)
parser.add_argument( parser.add_argument(
'--skip-cargo', '--skip-cargo',
action='store_true', action='store_true',
@@ -282,15 +272,10 @@ def get_features(args):
features = ['inline'] if not args.flutter else [] features = ['inline'] if not args.flutter else []
if args.hwcodec: if args.hwcodec:
features.append('hwcodec') features.append('hwcodec')
if args.gpucodec: if args.vram:
features.append('gpucodec') features.append('vram')
if args.flutter: if args.flutter:
features.append('flutter') features.append('flutter')
features.append('flutter_texture_render')
if args.flatpak:
features.append('flatpak')
if args.appimage:
features.append('appimage')
if args.unix_file_copy_paste: if args.unix_file_copy_paste:
features.append('unix-file-copy-paste') features.append('unix-file-copy-paste')
print("features:", features) print("features:", features)

View File

@@ -1,27 +1,29 @@
<p align="center"> <p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn"><br> <img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Máy chủ</a> • <a href="#free-public-servers">Server</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">Cấu trúc tệp tin</a> • <a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br> <a href="#snapshot">Snapshot</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-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-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-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-GR.md">Ελληνικά</a>]<br>
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và <a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b> <b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
</p> </p>
Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) Hãy trao đổi với chúng tôi qua: [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)
Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](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)
Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md). **RustDesk** luôn hoan nghênh mọi đóng góp từ mọi người. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu.
[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases) [**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" [<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid" alt="Get it on F-Droid"
@@ -29,28 +31,25 @@ Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để
## Dependencies ## Dependencies
Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bn cần tự tải về thư viện sciter. Phiên bản máy tính sử dụng __Flutter__ hoặc __Sciter__ (đã lỗi thời) cho giao diện người dùng (GUI). Hướng dẫn này chỉ áp dụng cho phiên bn Sciter, vì nó thân thiện và dễ bắt đầu hơn. Hãy kiểm tra [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) của chúng tôi để xây dựng phiên bản Flutter.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | Vui lòng tự tải thư viện `Sciter` về máy theo hướng dẫn cho từng hệ điều hành.
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
Phiên bản cho điện thoại sử dụng Flutter. Chúng tôi sẽ chuyển sang sử dụng Flutter thay cho Sciter cho phiên bản máy tính. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Cách để build ## Các bước build cơ bản
- Chuẩn bị môi trường phát triển Rust và môi trường build C++ - Chuẩn bị môi trường phát triển Rust và môi trường biên dịch C++
- Tải và cài [vcpkg](https://github.com/microsoft/vcpkg), và đặt biến môi trường `VCPKG_ROOT` sao cho đúng. - Tải và cài đặt [`vcpkg`](https://github.com/microsoft/vcpkg), và thiết lập biến môi trường `VCPKG_ROOT`.
- Đối với Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Đối với Linux/MacOS: vcpkg install libvpx libyuv opus aom
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
- Linux/MacOS: `vcpkg install libvpx libyuv opus aom`
- Chạy lệnh `cargo run` - Chạy lệnh `cargo run`
## [Build](https://rustdesk.com/docs/en/dev/build/) ## [Build](https://rustdesk.com/docs/en/dev/build/)
## Cách để build cho Linux ## Cách build cho Linux
### Ubuntu 18 (Debian 10) ### Ubuntu 18 (Debian 10)
@@ -70,7 +69,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
``` ```
### Cách cài vcpkg ### Cách cài đặt `vcpkg`
```sh ```sh
git clone https://github.com/microsoft/vcpkg git clone https://github.com/microsoft/vcpkg
@@ -82,7 +81,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom vcpkg/vcpkg install libvpx libyuv opus aom
``` ```
### Cách sửa lỗi libvpx (Dành cho hệ điều hành Fedora) ### Cách sửa lỗi `libvpx` (Dành cho hệ điều hành Fedora)
```sh ```sh
cd vcpkg/buildtrees/libvpx/src cd vcpkg/buildtrees/libvpx/src
@@ -95,7 +94,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd cd
``` ```
### Cách build ### Build
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -108,9 +107,9 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run VCPKG_ROOT=$HOME/vcpkg cargo run
``` ```
## Cách để build sử dụng Docker ## Cách build bằng Docker
Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer: Bắt đầu bằng cách sao chép repo này về máy tính của bạn và tạo Docker container:
```sh ```sh
git clone https://github.com/rustdesk/rustdesk git clone https://github.com/rustdesk/rustdesk
@@ -118,37 +117,37 @@ cd rustdesk
docker build -t "rustdesk-builder" . docker build -t "rustdesk-builder" .
``` ```
Rồi mỗi khi bạn chạy ứng dụng, thì hãy chạy lệnh này: Sau đó, mỗi khi bạn chạy ứng dụng, thì hãy chạy dòng lệnh sau:
```sh ```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
``` ```
Chú ý: Lần build đầu tiên có thể sẽ mất lâu hơn truớc khi các dependecies đuợc lưu lại, nhng lần build sau sẽ nhanh hơn. Hơn nũa, nếu bạn cần cung cấp các cài đặt lệnh khác cho lệnh build, bạn có thể đặt những cài đặt lệnh này vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh --release. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh: Lưu ý rằng **lần build đầu tiên có thể mất thời gian hơn trước khi các dependencies được lưu vào bộ nhớ cache**, nhưng các lần build sau sẽ nhanh hơn. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên với tùy chọn `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
```sh ```sh
target/debug/rustdesk target/debug/rustdesk
``` ```
Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh: Nếu bạn đang chạy bản build được tối ưu hóa, thì bạn có thể chạy với lệnh:
```sh ```sh
target/release/rustdesk target/release/rustdesk
``` ```
Hãy đảm bảo bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn. Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác của **cargo** như **install** hoặc **run** hiện không được hỗ trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính của bạn.
## Cấu trúc tệp tin ## Cấu trúc tệp tin
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: điều khiển máy tính/chuột trên các nền tảng khác nhau
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối với một peer
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed. - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành cho điện thoại - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
## Snapshot ## Snapshot

View File

@@ -8,17 +8,31 @@
"modules": [ "modules": [
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json", "shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
"xdotool.json", "xdotool.json",
{
"name": "pam",
"buildsystem": "simple",
"build-commands": [
"./configure --disable-selinux --prefix=/app && make -j4 install"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
}
]
},
{ {
"name": "rustdesk", "name": "rustdesk",
"buildsystem": "simple", "buildsystem": "simple",
"build-commands": [ "build-commands": [
"bsdtar -zxvf rustdesk-1.2.4.deb", "bsdtar -zxvf rustdesk.deb",
"tar -xvf ./data.tar.xz", "tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/", "cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk", "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop", "mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop", "mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop", "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg", "mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done" "for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
], ],
@@ -26,7 +40,7 @@
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"path": "../rustdesk-1.2.4.deb" "path": "./rustdesk.deb"
}, },
{ {
"type": "file", "type": "file",

View File

@@ -106,6 +106,6 @@ dependencies {
implementation "androidx.media:media:1.6.0" implementation "androidx.media:media:1.6.0"
implementation 'com.github.getActivity:XXPermissions:18.5' implementation 'com.github.getActivity:XXPermissions:18.5'
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
implementation 'com.caverock:androidsvg-aar:1.4'
} }
apply plugin: 'com.google.gms.google-services'

View File

@@ -1,40 +0,0 @@
{
"project_info": {
"project_number": "768133699366",
"firebase_url": "https://rustdesk.firebaseio.com",
"project_id": "rustdesk",
"storage_bucket": "rustdesk.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
"android_client_info": {
"package_name": "com.carriez.flutter_hbb"
}
},
"oauth_client": [
{
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@@ -3,6 +3,7 @@
package="com.carriez.flutter_hbb"> package="com.carriez.flutter_hbb">
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -80,6 +81,11 @@
android:name=".MainService" android:name=".MainService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="mediaProjection" /> android:foregroundServiceType="mediaProjection" />
<service
android:name=".FloatingWindowService"
android:enabled="true" />
<!-- <!--
Don't delete the meta-data below. Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java This is used by the Flutter tool to generate GeneratedPluginRegistrant.java

View File

@@ -0,0 +1,193 @@
package com.carriez.flutter_hbb
import ffi.FFI
import android.Manifest
import android.content.Context
import android.media.*
import android.content.pm.PackageManager
import android.media.projection.MediaProjection
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import android.os.Build
import android.util.Log
import kotlin.concurrent.thread
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
const val AUDIO_SAMPLE_RATE = 48000
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
private var audioRecorder: AudioRecord? = null
private var audioReader: AudioReader? = null
private var minBufferSize = 0
private var audioRecordStat = false
private var audioThread: Thread? = null
@RequiresApi(Build.VERSION_CODES.M)
fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return false
}
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
Log.d(logTag, "createAudioRecorder failed, no RECORD_AUDIO permission")
return false
}
var builder = AudioRecord.Builder()
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AUDIO_ENCODING)
.setSampleRate(AUDIO_SAMPLE_RATE)
.setChannelMask(AUDIO_CHANNEL_MASK).build()
);
if (inVoiceCall) {
builder.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
} else {
mediaProjection?.let {
var apcc = AudioPlaybackCaptureConfiguration.Builder(it)
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
.addMatchingUsage(AudioAttributes.USAGE_GAME)
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build();
builder.setAudioPlaybackCaptureConfig(apcc);
} ?: let {
Log.d(logTag, "createAudioRecorder failed, mediaProjection null")
return false
}
}
audioRecorder = builder.build()
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
return true
}
@RequiresApi(Build.VERSION_CODES.M)
private fun checkAudioReader() {
if (audioReader != null && minBufferSize != 0) {
return
}
// read f32 to byte , length * 4
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL_MASK,
AUDIO_ENCODING
)
if (minBufferSize == 0) {
Log.d(logTag, "get min buffer size fail!")
return
}
audioReader = AudioReader(minBufferSize, 4)
Log.d(logTag, "init audioData len:$minBufferSize")
}
@RequiresApi(Build.VERSION_CODES.M)
fun startAudioRecorder() {
checkAudioReader()
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
try {
FFI.setFrameRawEnable("audio", true)
audioRecorder!!.startRecording()
audioRecordStat = true
audioThread = thread {
while (audioRecordStat) {
audioReader!!.readSync(audioRecorder!!)?.let {
FFI.onAudioFrameUpdate(it)
}
}
// let's release here rather than onDestroy to avoid threading issue
audioRecorder?.release()
audioRecorder = null
minBufferSize = 0
FFI.setFrameRawEnable("audio", false)
Log.d(logTag, "Exit audio thread")
}
} catch (e: Exception) {
Log.d(logTag, "startAudioRecorder fail:$e")
}
} else {
Log.d(logTag, "startAudioRecorder fail")
}
}
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
if (!isSupportVoiceCall()) {
return false
}
// No need to check if video or audio is started here.
if (!switchToVoiceCall(mediaProjection)) {
return false
}
return true
}
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
// Return true if not supported, because is was not started.
if (!isSupportVoiceCall()) {
return true
}
if (isVideoStart()) {
switchOutVoiceCall(mediaProjection)
}
tryReleaseAudio()
return true
}
@RequiresApi(Build.VERSION_CODES.M)
fun switchToVoiceCall(mediaProjection: MediaProjection?): Boolean {
audioRecorder?.let {
if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
return true
}
}
audioRecordStat = false
audioThread?.join()
audioThread = null
if (!createAudioRecorder(true, mediaProjection)) {
Log.e(logTag, "createAudioRecorder fail")
return false
}
startAudioRecorder()
return true
}
@RequiresApi(Build.VERSION_CODES.M)
fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean {
audioRecorder?.let {
if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
return true
}
}
audioRecordStat = false
audioThread?.join()
if (!createAudioRecorder(false, mediaProjection)) {
Log.e(logTag, "createAudioRecorder fail")
return false
}
startAudioRecorder()
return true
}
fun tryReleaseAudio() {
if (isAudioStart() || isVideoStart()) {
return
}
audioRecordStat = false
audioThread?.join()
audioThread = null
}
fun destroy() {
Log.d(logTag, "destroy audio record handle")
audioRecordStat = false
audioThread?.join()
}
}

View File

@@ -0,0 +1,378 @@
package com.carriez.flutter_hbb
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.PixelFormat
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
import android.widget.ImageView
import android.widget.PopupMenu
import com.caverock.androidsvg.SVG
import ffi.FFI
import kotlin.math.abs
class FloatingWindowService : Service(), View.OnTouchListener {
private lateinit var windowManager: WindowManager
private lateinit var layoutParams: WindowManager.LayoutParams
private lateinit var floatingView: ImageView
private lateinit var originalDrawable: Drawable
private lateinit var leftHalfDrawable: Drawable
private lateinit var rightHalfDrawable: Drawable
private var dragging = false
private var lastDownX = 0f
private var lastDownY = 0f
private var viewCreated = false;
private var keepScreenOn = KeepScreenOn.DURING_CONTROLLED
companion object {
private val logTag = "floatingService"
private var firstCreate = true
private var viewWidth = 120
private var viewHeight = 120
private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed
private const val MAX_VIEW_SIZE = 320
private var viewUntouchable = false
private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed
private var customSvg = ""
private var lastLayoutX = 0
private var lastLayoutY = 0
private var lastOrientation = Configuration.ORIENTATION_UNDEFINED
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
try {
if (firstCreate) {
firstCreate = false
onFirstCreate(windowManager)
}
Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg")
createView(windowManager)
handler.postDelayed(runnable, 1000)
Log.d(logTag, "onCreate success")
} catch (e: Exception) {
Log.d(logTag, "onCreate failed: $e")
}
}
override fun onDestroy() {
super.onDestroy()
if (viewCreated) {
windowManager.removeView(floatingView)
}
handler.removeCallbacks(runnable)
}
@SuppressLint("ClickableViewAccessibility")
private fun createView(windowManager: WindowManager) {
floatingView = ImageView(this)
viewCreated = true
originalDrawable = resources.getDrawable(R.drawable.floating_window, null)
if (customSvg.isNotEmpty()) {
try {
val svg = SVG.getFromString(customSvg)
Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}");
// This make the svg render clear
svg.documentWidth = viewWidth * 1f
svg.documentHeight = viewHeight * 1f
originalDrawable = svg.renderToPicture().let {
BitmapDrawable(
resources,
Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888)
.also { bitmap ->
it.draw(Canvas(bitmap))
})
}
floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Log.d(logTag, "custom svg loaded")
} catch (e: Exception) {
e.printStackTrace()
}
}
val originalBitmap = Bitmap.createBitmap(
originalDrawable.intrinsicWidth,
originalDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(originalBitmap)
originalDrawable.setBounds(
0,
0,
originalDrawable.intrinsicWidth,
originalDrawable.intrinsicHeight
)
originalDrawable.draw(canvas)
val leftHalfBitmap = Bitmap.createBitmap(
originalBitmap,
0,
0,
originalDrawable.intrinsicWidth / 2,
originalDrawable.intrinsicHeight
)
val rightHalfBitmap = Bitmap.createBitmap(
originalBitmap,
originalDrawable.intrinsicWidth / 2,
0,
originalDrawable.intrinsicWidth / 2,
originalDrawable.intrinsicHeight
)
leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap)
rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap)
floatingView.setImageDrawable(rightHalfDrawable)
floatingView.setOnTouchListener(this)
floatingView.alpha = viewTransparency * 1f
var flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
if (viewUntouchable || viewTransparency == 0f) {
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
}
layoutParams = WindowManager.LayoutParams(
viewWidth / 2,
viewHeight,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
flags,
PixelFormat.TRANSLUCENT
)
layoutParams.gravity = Gravity.TOP or Gravity.START
layoutParams.x = lastLayoutX
layoutParams.y = lastLayoutY
val keepScreenOnOption = FFI.getLocalOption("keep-screen-on").lowercase()
keepScreenOn = when (keepScreenOnOption) {
"never" -> KeepScreenOn.NEVER
"service-on" -> KeepScreenOn.SERVICE_ON
else -> KeepScreenOn.DURING_CONTROLLED
}
Log.d(logTag, "keepScreenOn option: $keepScreenOnOption, value: $keepScreenOn")
updateKeepScreenOnLayoutParams()
windowManager.addView(floatingView, layoutParams)
moveToScreenSide()
}
private fun onFirstCreate(windowManager: WindowManager) {
val wh = getScreenSize(windowManager)
val w = wh.first
val h = wh.second
// size
FFI.getLocalOption("floating-window-size").let {
if (it.isNotEmpty()) {
try {
val size = it.toInt()
if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) {
viewWidth = size
viewHeight = size
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// untouchable
viewUntouchable = FFI.getLocalOption("floating-window-untouchable") == "Y"
// transparency
FFI.getLocalOption("floating-window-transparency").let {
if (it.isNotEmpty()) {
try {
val transparency = it.toInt()
if (transparency in 0..10) {
viewTransparency = transparency * 1f / 10
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// custom svg
FFI.getLocalOption("floating-window-svg").let {
if (it.isNotEmpty()) {
customSvg = it
}
}
// position
lastLayoutX = 0
lastLayoutY = (wh.second - viewHeight) / 2
lastOrientation = resources.configuration.orientation
}
private fun performClick() {
showPopupMenu()
}
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
dragging = false
lastDownX = event.rawX
lastDownY = event.rawY
}
MotionEvent.ACTION_UP -> {
val clickDragTolerance = 10f
if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) {
performClick()
} else {
moveToScreenSide()
}
}
MotionEvent.ACTION_MOVE -> {
val dx = event.rawX - lastDownX
val dy = event.rawY - lastDownY
// ignore too small fist start moving(some time is click)
if (!dragging && dx*dx+dy*dy < 25) {
return false
}
dragging = true
layoutParams.x = event.rawX.toInt()
layoutParams.y = event.rawY.toInt()
layoutParams.width = viewWidth
floatingView.setImageDrawable(originalDrawable)
windowManager.updateViewLayout(view, layoutParams)
lastLayoutX = layoutParams.x
lastLayoutY = layoutParams.y
}
}
return false
}
private fun moveToScreenSide(center: Boolean = false) {
val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
val wh = getScreenSize(windowManager)
val w = wh.first
if (layoutParams.x < w / 2) {
layoutParams.x = 0
floatingView.setImageDrawable(rightHalfDrawable)
} else {
layoutParams.x = w - viewWidth / 2
floatingView.setImageDrawable(leftHalfDrawable)
}
if (center) {
layoutParams.y = (wh.second - viewHeight) / 2
}
layoutParams.width = viewWidth / 2
windowManager.updateViewLayout(floatingView, layoutParams)
lastLayoutX = layoutParams.x
lastLayoutY = layoutParams.y
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (newConfig.orientation != lastOrientation) {
lastOrientation = newConfig.orientation
val wh = getScreenSize(windowManager)
Log.d(logTag, "orientation: $lastOrientation, screen size: ${wh.first} x ${wh.second}")
val newW = wh.first
val newH = wh.second
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
// Proportional change
layoutParams.x = (layoutParams.x.toFloat() / newH.toFloat() * newW.toFloat()).toInt()
layoutParams.y = (layoutParams.y.toFloat() / newW.toFloat() * newH.toFloat()).toInt()
}
moveToScreenSide()
}
}
private fun showPopupMenu() {
val popupMenu = PopupMenu(this, floatingView)
val idShowRustDesk = 0
popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk"))
val idStopService = 1
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
idShowRustDesk -> {
openMainActivity()
true
}
idStopService -> {
stopMainService()
true
}
else -> false
}
}
popupMenu.setOnDismissListener {
moveToScreenSide()
}
popupMenu.show()
}
private fun openMainActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
)
try {
pendingIntent.send()
} catch (e: PendingIntent.CanceledException) {
e.printStackTrace()
}
}
private fun stopMainService() {
MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null)
}
enum class KeepScreenOn {
NEVER,
DURING_CONTROLLED,
SERVICE_ON,
}
private val handler = Handler(Looper.getMainLooper())
private val runnable = object : Runnable {
override fun run() {
if (updateKeepScreenOnLayoutParams()) {
windowManager.updateViewLayout(floatingView, layoutParams)
}
handler.postDelayed(this, 1000) // 1000 milliseconds = 1 second
}
}
private fun updateKeepScreenOnLayoutParams(): Boolean {
val oldOn = layoutParams.flags and FLAG_KEEP_SCREEN_ON != 0
val newOn = keepScreenOn == KeepScreenOn.SERVICE_ON || (keepScreenOn == KeepScreenOn.DURING_CONTROLLED && MainService.isStart)
if (oldOn != newOn) {
Log.d(logTag, "change keep screen on to $newOn")
if (newOn) {
layoutParams.flags = layoutParams.flags or FLAG_KEEP_SCREEN_ON
} else {
layoutParams.flags = layoutParams.flags and FLAG_KEEP_SCREEN_ON.inv()
}
return true
}
return false
}
}

View File

@@ -7,6 +7,8 @@ package com.carriez.flutter_hbb
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG * Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
*/ */
import ffi.FFI
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -15,10 +17,20 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import android.media.MediaCodecInfo
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
import android.media.MediaCodecList
import android.media.MediaFormat
import android.util.DisplayMetrics
import androidx.annotation.RequiresApi
import org.json.JSONArray
import org.json.JSONObject
import com.hjq.permissions.XXPermissions import com.hjq.permissions.XXPermissions
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlin.concurrent.thread
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
@@ -30,6 +42,9 @@ class MainActivity : FlutterActivity() {
private val logTag = "mMainActivity" private val logTag = "mMainActivity"
private var mainService: MainService? = null private var mainService: MainService? = null
private var isAudioStart = false
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
if (MainService.isReady) { if (MainService.isReady) {
@@ -42,6 +57,7 @@ class MainActivity : FlutterActivity() {
channelTag channelTag
) )
initFlutterChannel(flutterMethodChannel!!) initFlutterChannel(flutterMethodChannel!!)
thread { setCodecInfo() }
} }
override fun onResume() { override fun onResume() {
@@ -217,10 +233,159 @@ class MainActivity : FlutterActivity() {
result.success(false) result.success(false)
} }
} }
GET_VALUE -> {
if (call.arguments is String) {
if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) {
result.success(isSupportVoiceCall())
} else {
result.error("-1", "No such key", null)
}
} else {
result.success(null)
}
}
"on_voice_call_started" -> {
onVoiceCallStarted()
}
"on_voice_call_closed" -> {
onVoiceCallClosed()
}
else -> { else -> {
result.error("-1", "No such method", null) result.error("-1", "No such method", null)
} }
} }
} }
} }
private fun setCodecInfo() {
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codecs = codecList.codecInfos
val codecArray = JSONArray()
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val wh = getScreenSize(windowManager)
var w = wh.first
var h = wh.second
val align = 64
w = (w + align - 1) / align * align
h = (h + align - 1) / align * align
codecs.forEach { codec ->
val codecObject = JSONObject()
codecObject.put("name", codec.name)
codecObject.put("is_encoder", codec.isEncoder)
var hw: Boolean? = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
hw = codec.isHardwareAccelerated
} else {
// https://chromium.googlesource.com/external/webrtc/+/HEAD/sdk/android/src/java/org/webrtc/MediaCodecUtils.java#29
// https://chromium.googlesource.com/external/webrtc/+/master/sdk/android/api/org/webrtc/HardwareVideoEncoderFactory.java#229
if (listOf("OMX.google.", "OMX.SEC.", "c2.android").any { codec.name.startsWith(it, true) }) {
hw = false
} else if (listOf("c2.qti", "OMX.qcom.video", "OMX.Exynos", "OMX.hisi", "OMX.MTK", "OMX.Intel", "OMX.Nvidia").any { codec.name.startsWith(it, true) }) {
hw = true
}
}
if (hw != true) {
return@forEach
}
codecObject.put("hw", hw)
var mime_type = ""
codec.supportedTypes.forEach { type ->
if (listOf("video/avc", "video/hevc").contains(type)) { // "video/x-vnd.on2.vp8", "video/x-vnd.on2.vp9", "video/av01"
mime_type = type;
}
}
if (mime_type.isNotEmpty()) {
codecObject.put("mime_type", mime_type)
val caps = codec.getCapabilitiesForType(mime_type)
if (codec.isEncoder) {
// Encoders max_height and max_width are interchangeable
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
return@forEach
}
}
codecObject.put("min_width", caps.videoCapabilities.supportedWidths.lower)
codecObject.put("max_width", caps.videoCapabilities.supportedWidths.upper)
codecObject.put("min_height", caps.videoCapabilities.supportedHeights.lower)
codecObject.put("max_height", caps.videoCapabilities.supportedHeights.upper)
val surface = caps.colorFormats.contains(COLOR_FormatSurface);
codecObject.put("surface", surface)
val nv12 = caps.colorFormats.contains(COLOR_FormatYUV420SemiPlanar)
codecObject.put("nv12", nv12)
if (!(nv12 || surface)) {
return@forEach
}
codecObject.put("min_bitrate", caps.videoCapabilities.bitrateRange.lower / 1000)
codecObject.put("max_bitrate", caps.videoCapabilities.bitrateRange.upper / 1000)
if (!codec.isEncoder) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
codecObject.put("low_latency", caps.isFeatureSupported(MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency))
}
}
if (!codec.isEncoder) {
return@forEach
}
codecArray.put(codecObject)
}
}
val result = JSONObject()
result.put("version", Build.VERSION.SDK_INT)
result.put("w", w)
result.put("h", h)
result.put("codecs", codecArray)
FFI.setCodecInfo(result.toString())
}
private fun onVoiceCallStarted() {
var ok = false
mainService?.let {
ok = it.onVoiceCallStarted()
} ?: let {
isAudioStart = true
ok = audioRecordHandle.onVoiceCallStarted(null)
}
if (!ok) {
// Rarely happens, So we just add log and msgbox here.
Log.e(logTag, "onVoiceCallStarted fail")
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to start voice call."))
} else {
Log.d(logTag, "onVoiceCallStarted success")
}
}
private fun onVoiceCallClosed() {
var ok = false
mainService?.let {
ok = it.onVoiceCallClosed()
} ?: let {
isAudioStart = false
ok = audioRecordHandle.onVoiceCallClosed(null)
}
if (!ok) {
// Rarely happens, So we just add log and msgbox here.
Log.e(logTag, "onVoiceCallClosed fail")
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to stop voice call."))
} else {
Log.d(logTag, "onVoiceCallClosed success")
}
}
override fun onStop() {
super.onStop()
val disableFloatingWindow = FFI.getLocalOption("disable-floating-window") == "Y"
if (!disableFloatingWindow && MainService.isReady) {
startService(Intent(this, FloatingWindowService::class.java))
}
}
override fun onStart() {
super.onStart()
stopService(Intent(this, FloatingWindowService::class.java))
}
} }

View File

@@ -1,5 +1,7 @@
package com.carriez.flutter_hbb package com.carriez.flutter_hbb
import ffi.FFI
/** /**
* Capture screen,get video and audio,send to rust. * Capture screen,get video and audio,send to rust.
* Dispatch notifications * Dispatch notifications
@@ -52,27 +54,19 @@ const val NOTIFY_ID_OFFSET = 100
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9 const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
// video const // video const
const val MAX_SCREEN_SIZE = 1200 const val MAX_SCREEN_SIZE = 1200
const val VIDEO_KEY_BIT_RATE = 1024_000 const val VIDEO_KEY_BIT_RATE = 1024_000
const val VIDEO_KEY_FRAME_RATE = 30 const val VIDEO_KEY_FRAME_RATE = 30
// audio const
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
const val AUDIO_SAMPLE_RATE = 48000
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
class MainService : Service() { class MainService : Service() {
init {
System.loadLibrary("rustdesk")
}
@Keep @Keep
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) { fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off // turn on screen with LIFT_DOWN when screen off
if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) { if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
Log.d(logTag, "Turn on Screen, WakeLock release") Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release() wakeLock.release()
@@ -81,10 +75,10 @@ class MainService : Service() {
wakeLock.acquire(5000) wakeLock.acquire(5000)
} else { } else {
when (kind) { when (kind) {
"touch" -> { 0 -> { // touch
InputService.ctx?.onTouchInput(mask, x, y) InputService.ctx?.onTouchInput(mask, x, y)
} }
"mouse" -> { 1 -> { // mouse
InputService.ctx?.onMouseInput(mask, x, y) InputService.ctx?.onMouseInput(mask, x, y)
} }
else -> { else -> {
@@ -109,6 +103,9 @@ class MainService : Service() {
put("scale",SCREEN_INFO.scale) put("scale",SCREEN_INFO.scale)
}.toString() }.toString()
} }
"is_start" -> {
isStart.toString()
}
else -> "" else -> ""
} }
} }
@@ -141,10 +138,51 @@ class MainService : Service() {
e.printStackTrace() e.printStackTrace()
} }
} }
"update_voice_call_state" -> {
try {
val jsonObject = JSONObject(arg1)
val id = jsonObject["id"] as Int
val username = jsonObject["name"] as String
val peerId = jsonObject["peer_id"] as String
val inVoiceCall = jsonObject["in_voice_call"] as Boolean
val incomingVoiceCall = jsonObject["incoming_voice_call"] as Boolean
if (!inVoiceCall) {
if (incomingVoiceCall) {
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
} else {
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
Log.e(logTag, "switchOutVoiceCall fail")
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to switch out voice call."))
}
}
} else {
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
Log.e(logTag, "switchToVoiceCall fail")
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
"type" to "custom-nook-nocancel-hasclose-error",
"title" to "Voice call",
"text" to "Failed to switch to voice call."))
}
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
"stop_capture" -> { "stop_capture" -> {
Log.d(logTag, "from rust:stop_capture") Log.d(logTag, "from rust:stop_capture")
stopCapture() stopCapture()
} }
"half_scale" -> {
val halfScale = arg1.toBoolean()
if (isHalfScale != halfScale) {
isHalfScale = halfScale
updateScreenInfo(resources.configuration.orientation)
}
}
else -> { else -> {
} }
} }
@@ -156,38 +194,23 @@ class MainService : Service() {
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager } private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")} private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
// jvm call rust
private external fun init(ctx: Context)
/// When app start on boot, app_dir will not be passed from flutter
/// so pass a app_dir here to rust server
private external fun startServer(app_dir: String)
private external fun startService()
private external fun onVideoFrameUpdate(buf: ByteBuffer)
private external fun onAudioFrameUpdate(buf: ByteBuffer)
private external fun translateLocale(localeName: String, input: String): String
private external fun refreshScreen()
private external fun setFrameRawEnable(name: String, value: Boolean)
// private external fun sendVp9(data: ByteArray)
private fun translate(input: String): String {
Log.d(logTag, "translate:$LOCAL_NAME")
return translateLocale(LOCAL_NAME, input)
}
companion object { companion object {
private var _isReady = false // media permission ready status private var _isReady = false // media permission ready status
private var _isStart = false // screen capture start status private var _isStart = false // screen capture start status
private var _isAudioStart = false // audio capture start status
val isReady: Boolean val isReady: Boolean
get() = _isReady get() = _isReady
val isStart: Boolean val isStart: Boolean
get() = _isStart get() = _isStart
val isAudioStart: Boolean
get() = _isAudioStart
} }
private val logTag = "LOG_SERVICE" private val logTag = "LOG_SERVICE"
private val useVP9 = false private val useVP9 = false
private val binder = LocalBinder() private val binder = LocalBinder()
private var reuseVirtualDisplay = Build.VERSION.SDK_INT > 33
// video // video
private var mediaProjection: MediaProjection? = null private var mediaProjection: MediaProjection? = null
@@ -198,10 +221,7 @@ class MainService : Service() {
private var virtualDisplay: VirtualDisplay? = null private var virtualDisplay: VirtualDisplay? = null
// audio // audio
private var audioRecorder: AudioRecord? = null private val audioRecordHandle = AudioRecordHandle(this, { isStart }, { isAudioStart })
private var audioReader: AudioReader? = null
private var minBufferSize = 0
private var audioRecordStat = false
// notification // notification
private lateinit var notificationManager: NotificationManager private lateinit var notificationManager: NotificationManager
@@ -210,8 +230,8 @@ class MainService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(logTag,"MainService onCreate") Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay")
init(this) FFI.init(this)
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply { HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
start() start()
serviceLooper = looper serviceLooper = looper
@@ -223,16 +243,18 @@ class MainService : Service() {
// keep the config dir same with flutter // keep the config dir same with flutter
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: "" val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
startServer(configPath) FFI.startServer(configPath, "")
createForegroundNotification() createForegroundNotification()
} }
override fun onDestroy() { override fun onDestroy() {
checkMediaPermission() checkMediaPermission()
stopService(Intent(this, FloatingWindowService::class.java))
super.onDestroy() super.onDestroy()
} }
private var isHalfScale: Boolean? = null;
private fun updateScreenInfo(orientation: Int) { private fun updateScreenInfo(orientation: Int) {
var w: Int var w: Int
var h: Int var h: Int
@@ -265,7 +287,7 @@ class MainService : Service() {
Log.d(logTag,"updateScreenInfo:w:$w,h:$h") Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
var scale = 1 var scale = 1
if (w != 0 && h != 0) { if (w != 0 && h != 0) {
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) { if (isHalfScale == true && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
scale = 2 scale = 2
w /= scale w /= scale
h /= scale h /= scale
@@ -278,7 +300,7 @@ class MainService : Service() {
SCREEN_INFO.dpi = dpi SCREEN_INFO.dpi = dpi
if (isStart) { if (isStart) {
stopCapture() stopCapture()
refreshScreen() FFI.refreshScreen()
startCapture() startCapture()
} }
} }
@@ -306,7 +328,7 @@ class MainService : Service() {
createForegroundNotification() createForegroundNotification()
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) { if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
startService() FFI.startService()
} }
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
val mediaProjectionManager = val mediaProjectionManager =
@@ -354,12 +376,13 @@ class MainService : Service() {
).apply { ).apply {
setOnImageAvailableListener({ imageReader: ImageReader -> setOnImageAvailableListener({ imageReader: ImageReader ->
try { try {
// If not call acquireLatestImage, listener will not be called again
imageReader.acquireLatestImage().use { image -> imageReader.acquireLatestImage().use { image ->
if (image == null) return@setOnImageAvailableListener if (image == null || !isStart) return@setOnImageAvailableListener
val planes = image.planes val planes = image.planes
val buffer = planes[0].buffer val buffer = planes[0].buffer
buffer.rewind() buffer.rewind()
onVideoFrameUpdate(buffer) FFI.onVideoFrameUpdate(buffer)
} }
} catch (ignored: java.lang.Exception) { } catch (ignored: java.lang.Exception) {
} }
@@ -370,6 +393,14 @@ class MainService : Service() {
} }
} }
fun onVoiceCallStarted(): Boolean {
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
}
fun onVoiceCallClosed(): Boolean {
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
}
fun startCapture(): Boolean { fun startCapture(): Boolean {
if (isStart) { if (isStart) {
return true return true
@@ -378,6 +409,7 @@ class MainService : Service() {
Log.w(logTag, "startCapture fail,mediaProjection is null") Log.w(logTag, "startCapture fail,mediaProjection is null")
return false return false
} }
updateScreenInfo(resources.configuration.orientation) updateScreenInfo(resources.configuration.orientation)
Log.d(logTag, "Start Capture") Log.d(logTag, "Start Capture")
surface = createSurface() surface = createSurface()
@@ -389,51 +421,71 @@ class MainService : Service() {
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startAudioRecorder() if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
Log.d(logTag, "createAudioRecorder fail")
} else {
Log.d(logTag, "audio recorder start")
audioRecordHandle.startAudioRecorder()
}
} }
checkMediaPermission() checkMediaPermission()
_isStart = true _isStart = true
setFrameRawEnable("video",true) FFI.setFrameRawEnable("video",true)
setFrameRawEnable("audio",true)
return true return true
} }
@Synchronized @Synchronized
fun stopCapture() { fun stopCapture() {
Log.d(logTag, "Stop Capture") Log.d(logTag, "Stop Capture")
setFrameRawEnable("video",false) FFI.setFrameRawEnable("video",false)
setFrameRawEnable("audio",false)
_isStart = false _isStart = false
// release video // release video
virtualDisplay?.release() if (reuseVirtualDisplay) {
surface?.release() // The virtual display video projection can be paused by calling `setSurface(null)`.
// https://developer.android.com/reference/android/hardware/display/VirtualDisplay.Callback
// https://learn.microsoft.com/en-us/dotnet/api/android.hardware.display.virtualdisplay.callback.onpaused?view=net-android-34.0
virtualDisplay?.setSurface(null)
} else {
virtualDisplay?.release()
}
// suface needs to be release after `imageReader.close()` to imageReader access released surface
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
imageReader?.close() imageReader?.close()
imageReader = null
videoEncoder?.let { videoEncoder?.let {
it.signalEndOfInputStream() it.signalEndOfInputStream()
it.stop() it.stop()
it.release() it.release()
} }
virtualDisplay = null if (!reuseVirtualDisplay) {
virtualDisplay = null
}
videoEncoder = null videoEncoder = null
// suface needs to be release after `imageReader.close()` to imageReader access released surface
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
surface?.release()
// release audio // release audio
audioRecordStat = false _isAudioStart = false
audioRecorder?.release() audioRecordHandle.tryReleaseAudio()
audioRecorder = null
minBufferSize = 0
} }
fun destroy() { fun destroy() {
Log.d(logTag, "destroy service") Log.d(logTag, "destroy service")
_isReady = false _isReady = false
_isAudioStart = false
stopCapture() stopCapture()
imageReader?.close()
imageReader = null if (reuseVirtualDisplay) {
virtualDisplay?.release()
virtualDisplay = null
}
mediaProjection = null mediaProjection = null
checkMediaPermission() checkMediaPermission()
stopForeground(true) stopForeground(true)
stopService(Intent(this, FloatingWindowService::class.java))
stopSelf() stopSelf()
} }
@@ -459,11 +511,7 @@ class MainService : Service() {
Log.d(logTag, "startRawVideoRecorder failed,surface is null") Log.d(logTag, "startRawVideoRecorder failed,surface is null")
return return
} }
virtualDisplay = mp.createVirtualDisplay( createOrSetVirtualDisplay(mp, surface!!)
"RustDeskVD",
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, null, null
)
} }
private fun startVP9VideoRecorder(mp: MediaProjection) { private fun startVP9VideoRecorder(mp: MediaProjection) {
@@ -475,11 +523,28 @@ class MainService : Service() {
} }
it.setCallback(cb) it.setCallback(cb)
it.start() it.start()
virtualDisplay = mp.createVirtualDisplay( createOrSetVirtualDisplay(mp, surface!!)
"RustDeskVD", }
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, }
surface, null, null
) // https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
try {
virtualDisplay?.let {
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
it.setSurface(s)
} ?: let {
virtualDisplay = mp.createVirtualDisplay(
"RustDeskVD",
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
s, null, null
)
}
} catch (e: SecurityException) {
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
// This initiates a prompt dialog for the user to confirm screen projection.
requestMediaProjection()
} }
} }
@@ -507,7 +572,6 @@ class MainService : Service() {
} }
} }
private fun createMediaCodec() { private fun createMediaCodec() {
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE") Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE) videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
@@ -527,76 +591,6 @@ class MainService : Service() {
} }
} }
@RequiresApi(Build.VERSION_CODES.M)
private fun startAudioRecorder() {
checkAudioRecorder()
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
try {
audioRecorder!!.startRecording()
audioRecordStat = true
thread {
while (audioRecordStat) {
audioReader!!.readSync(audioRecorder!!)?.let {
onAudioFrameUpdate(it)
}
}
Log.d(logTag, "Exit audio thread")
}
} catch (e: Exception) {
Log.d(logTag, "startAudioRecorder fail:$e")
}
} else {
Log.d(logTag, "startAudioRecorder fail")
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun checkAudioRecorder() {
if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
return
}
// read f32 to byte , length * 4
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL_MASK,
AUDIO_ENCODING
)
if (minBufferSize == 0) {
Log.d(logTag, "get min buffer size fail!")
return
}
audioReader = AudioReader(minBufferSize, 4)
Log.d(logTag, "init audioData len:$minBufferSize")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mediaProjection?.let {
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
.addMatchingUsage(AudioAttributes.USAGE_GAME)
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
return
}
audioRecorder = AudioRecord.Builder()
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AUDIO_ENCODING)
.setSampleRate(AUDIO_SAMPLE_RATE)
.setChannelMask(AUDIO_CHANNEL_MASK).build()
)
.setAudioPlaybackCaptureConfig(apcc)
.setBufferSizeInBytes(minBufferSize).build()
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
return
}
}
Log.d(logTag, "createAudioRecorder fail")
}
private fun initNotification() { private fun initNotification() {
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -681,6 +675,21 @@ class MainService : Service() {
notificationManager.notify(getClientNotifyID(clientID), notification) notificationManager.notify(getClientNotifyID(clientID), notification)
} }
private fun voiceCallRequestNotification(
clientID: Int,
type: String,
username: String,
peerId: String
) {
val notification = notificationBuilder
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentTitle(translate("Do you accept?"))
.setContentText("$type:$username-$peerId")
.build()
notificationManager.notify(getClientNotifyID(clientID), notification)
}
private fun getClientNotifyID(clientID: Int): Int { private fun getClientNotifyID(clientID: Int): Int {
return clientID + NOTIFY_ID_OFFSET return clientID + NOTIFY_ID_OFFSET
} }

View File

@@ -15,10 +15,14 @@ import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.provider.Settings.* import android.provider.Settings.*
import android.util.DisplayMetrics
import android.util.Log
import android.view.WindowManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions import com.hjq.permissions.XXPermissions
import ffi.FFI
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
@@ -43,6 +47,9 @@ const val START_ACTION = "start_action"
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt" const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt" const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir" const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
const val GET_VALUE = "get_value"
const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL"
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES" const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT" const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
@@ -56,6 +63,11 @@ data class Info(
var width: Int, var height: Int, var scale: Int, var dpi: Int var width: Int, var height: Int, var scale: Int, var dpi: Int
) )
fun isSupportVoiceCall(): Boolean {
// https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
fun requestPermission(context: Context, type: String) { fun requestPermission(context: Context, type: String) {
XXPermissions.with(context) XXPermissions.with(context)
.permission(type) .permission(type)
@@ -120,3 +132,26 @@ class AudioReader(val bufSize: Int, private val maxFrames: Int) {
} }
} }
} }
fun getScreenSize(windowManager: WindowManager) : Pair<Int, Int>{
var w = 0
var h = 0
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val m = windowManager.maximumWindowMetrics
w = m.bounds.width()
h = m.bounds.height()
} else {
val dm = DisplayMetrics()
windowManager.defaultDisplay.getRealMetrics(dm)
w = dm.widthPixels
h = dm.heightPixels
}
return Pair(w, h)
}
fun translate(input: String): String {
Log.d("common", "translate:$LOCAL_NAME")
return FFI.translateLocale(LOCAL_NAME, input)
}

View File

@@ -0,0 +1,23 @@
// ffi.kt
package ffi
import android.content.Context
import java.nio.ByteBuffer
object FFI {
init {
System.loadLibrary("rustdesk")
}
external fun init(ctx: Context)
external fun startServer(app_dir: String, custom_client_config: String)
external fun startService()
external fun onVideoFrameUpdate(buf: ByteBuffer)
external fun onAudioFrameUpdate(buf: ByteBuffer)
external fun translateLocale(localeName: String, input: String): String
external fun refreshScreen()
external fun setFrameRawEnable(name: String, value: Boolean)
external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String
}

View File

@@ -0,0 +1,7 @@
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:height="320dp" android:viewportHeight="32" android:viewportWidth="32" android:width="320dp">
<path android:fillColor="#ffffff" android:pathData="M16,0L16,0A16,16 0,0 1,32 16L32,16A16,16 0,0 1,16 32L16,32A16,16 0,0 1,0 16L0,16A16,16 0,0 1,16 0z" android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#1a1a1a" android:pathData="m23.89,10.135 l-1.807,1.795c-0.318,0.285 -0.472,0.744 -0.293,1.131 1.204,2.518 0.747,5.52 -1.228,7.494 -1.976,1.973 -4.981,2.429 -7.502,1.226 -0.371,-0.166 -0.807,-0.025 -1.093,0.265l-1.836,1.833c-0.216,0.211 -0.322,0.51 -0.288,0.809 0.034,0.3 0.206,0.567 0.463,0.723 4.326,2.618 9.882,1.951 13.463,-1.618 3.581,-3.568 4.264,-9.115 1.655,-13.443 -0.15,-0.263 -0.414,-0.442 -0.714,-0.484 -0.3,-0.043 -0.603,0.058 -0.819,0.269zM8.265,8.184c-3.599,3.554 -4.304,9.103 -1.709,13.441 0.15,0.264 0.413,0.443 0.714,0.485 0.3,0.042 0.603,-0.058 0.82,-0.27l1.797,-1.785c0.325,-0.285 0.484,-0.749 0.303,-1.141 -1.204,-2.518 -0.748,-5.52 1.228,-7.493 1.975,-1.973 4.981,-2.429 7.502,-1.227 0.367,0.165 0.797,0.028 1.084,-0.254l1.846,-1.844c0.216,-0.211 0.322,-0.509 0.288,-0.809 -0.035,-0.299 -0.206,-0.566 -0.463,-0.723 -4.334,-2.596 -9.881,-1.908 -13.448,1.668z" android:strokeWidth="0.987992"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="-186 -186 1365 1365"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -139,4 +139,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156 PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
COCOAPODS: 1.12.1 COCOAPODS: 1.15.2

View File

@@ -159,7 +159,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1430; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1510"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.networking.wifi-info</key> <key>com.apple.developer.networking.wifi-info</key>
<true/> <true/>
</dict> </dict>

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash #!/usr/bin/env bash
cargo build --features flutter --release --target aarch64-apple-ios --lib cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib

View File

@@ -12,13 +12,13 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:flutter_hbb/utils/platform_channel.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:uni_links/uni_links.dart'; import 'package:uni_links/uni_links.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -55,6 +55,12 @@ var isMobile = isAndroid || isIOS;
var version = ''; var version = '';
int androidVersion = 0; int androidVersion = 0;
// Only used on Linux.
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux.
// https://stackoverflow.com/questions/8193613/gtk-window-resize-disable-without-going-back-to-default
// So we need to use this flag to enable/disable resizable.
bool _linuxWindowResizable = true;
/// only available for Windows target /// only available for Windows target
int windowsBuildNumber = 0; int windowsBuildNumber = 0;
DesktopType? desktopType; DesktopType? desktopType;
@@ -544,7 +550,8 @@ class MyTheme {
Get.changeThemeMode(mode); Get.changeThemeMode(mode);
if (desktopType == DesktopType.main || isAndroid || isIOS) { if (desktopType == DesktopType.main || isAndroid || isIOS) {
if (mode == ThemeMode.system) { if (mode == ThemeMode.system) {
await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: defaultOptionTheme);
} else { } else {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString()); key: kCommConfKeyTheme, value: mode.toShortString());
@@ -711,7 +718,21 @@ class OverlayDialogManager {
int _tagCount = 0; int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry; OverlayEntry? _mobileActionsOverlayEntry;
RxBool mobileActionsOverlayVisible = false.obs; RxBool mobileActionsOverlayVisible = true.obs;
setMobileActionsOverlayVisible(bool v, {store = true}) {
if (store) {
bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N');
}
// No need to read the value from local storage after setting it.
// It better to toggle the value directly.
mobileActionsOverlayVisible.value = v;
}
loadMobileActionsOverlayVisible() {
mobileActionsOverlayVisible.value =
bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N';
}
void setOverlayState(OverlayKeyState overlayKeyState) { void setOverlayState(OverlayKeyState overlayKeyState) {
_overlayKeyState = overlayKeyState; _overlayKeyState = overlayKeyState;
@@ -852,40 +873,20 @@ class OverlayDialogManager {
final overlayState = _overlayKeyState.state; final overlayState = _overlayKeyState.state;
if (overlayState == null) return; if (overlayState == null) return;
// compute overlay position final overlay = makeMobileActionsOverlayEntry(
final screenW = MediaQuery.of(globalKey.currentContext!).size.width; () => hideMobileActionsOverlay(),
final screenH = MediaQuery.of(globalKey.currentContext!).size.height; ffi: ffi,
const double overlayW = 200; );
const double overlayH = 45;
final left = (screenW - overlayW) / 2;
final top = screenH - overlayH - 80;
final overlay = OverlayEntry(builder: (context) {
final session = ffi ?? gFFI;
return DraggableMobileActions(
position: Offset(left, top),
width: overlayW,
height: overlayH,
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
onRecentPressed: () async {
session.inputModel.sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
session.inputModel.sendMouse('up', MouseButtons.wheel);
},
onHidePressed: () => hideMobileActionsOverlay(),
);
});
overlayState.insert(overlay); overlayState.insert(overlay);
_mobileActionsOverlayEntry = overlay; _mobileActionsOverlayEntry = overlay;
mobileActionsOverlayVisible.value = true; setMobileActionsOverlayVisible(true);
} }
void hideMobileActionsOverlay() { void hideMobileActionsOverlay({store = true}) {
if (_mobileActionsOverlayEntry != null) { if (_mobileActionsOverlayEntry != null) {
_mobileActionsOverlayEntry!.remove(); _mobileActionsOverlayEntry!.remove();
_mobileActionsOverlayEntry = null; _mobileActionsOverlayEntry = null;
mobileActionsOverlayVisible.value = false; setMobileActionsOverlayVisible(false, store: store);
return; return;
} }
} }
@@ -903,6 +904,51 @@ class OverlayDialogManager {
} }
} }
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
makeMobileActions(BuildContext context, double s) {
final scale = s < 0.85 ? 0.85 : s;
final session = ffi ?? gFFI;
const double overlayW = 200;
const double overlayH = 45;
computeOverlayPosition() {
final screenW = MediaQuery.of(context).size.width;
final screenH = MediaQuery.of(context).size.height;
final left = (screenW - overlayW * scale) / 2;
final top = screenH - (overlayH + 80) * scale;
return Offset(left, top);
}
if (draggablePositions.mobileActions.isInvalid()) {
draggablePositions.mobileActions.update(computeOverlayPosition());
} else {
draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale);
}
return DraggableMobileActions(
scale: scale,
position: draggablePositions.mobileActions,
width: overlayW,
height: overlayH,
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
onRecentPressed: () async {
session.inputModel.sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
session.inputModel.sendMouse('up', MouseButtons.wheel);
},
onHidePressed: onHide,
);
}
return OverlayEntry(builder: (context) {
if (isDesktop) {
final c = Provider.of<CanvasModel>(context);
return makeMobileActions(context, c.scale * 2.0);
} else {
return makeMobileActions(globalKey.currentContext!, 1.0);
}
});
}
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) { void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
final overlayState = globalKey.currentState?.overlay; final overlayState = globalKey.currentState?.overlay;
if (overlayState == null) return; if (overlayState == null) return;
@@ -982,7 +1028,8 @@ class CustomAlertDialog extends StatelessWidget {
return KeyEventResult.handled; // avoid TextField exception on escape return KeyEventResult.handled; // avoid TextField exception on escape
} else if (!tabTapped && } else if (!tabTapped &&
onSubmit != null && onSubmit != null &&
key.logicalKey == LogicalKeyboardKey.enter) { (key.logicalKey == LogicalKeyboardKey.enter ||
key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
if (key is RawKeyDownEvent) onSubmit?.call(); if (key is RawKeyDownEvent) onSubmit?.call();
return KeyEventResult.handled; return KeyEventResult.handled;
} else if (key.logicalKey == LogicalKeyboardKey.tab) { } else if (key.logicalKey == LogicalKeyboardKey.tab) {
@@ -1395,14 +1442,16 @@ String translate(String name) {
return platformFFI.translate(name, localeName); return platformFFI.translate(name, localeName);
} }
// This function must be kept the same as the one in rust and sciter code.
// rust: libs/hbb_common/src/config.rs -> option2bool()
// sciter: Does not have the function, but it should be kept the same.
bool option2bool(String option, String value) { bool option2bool(String option, String value) {
bool res; bool res;
if (option.startsWith("enable-")) { if (option.startsWith("enable-")) {
res = value != "N"; res = value != "N";
} else if (option.startsWith("allow-") || } else if (option.startsWith("allow-") ||
option == "stop-service" || option == kOptionStopService ||
option == "direct-server" || option == kOptionDirectServer ||
option == "stop-rendezvous-service" ||
option == kOptionForceAlwaysRelay) { option == kOptionForceAlwaysRelay) {
res = value == "Y"; res = value == "Y";
} else { } else {
@@ -1415,13 +1464,12 @@ bool option2bool(String option, String value) {
String bool2option(String option, bool b) { String bool2option(String option, bool b) {
String res; String res;
if (option.startsWith('enable-')) { if (option.startsWith('enable-')) {
res = b ? '' : 'N'; res = b ? defaultOptionYes : 'N';
} else if (option.startsWith('allow-') || } else if (option.startsWith('allow-') ||
option == "stop-service" || option == kOptionStopService ||
option == "direct-server" || option == kOptionDirectServer ||
option == "stop-rendezvous-service" ||
option == kOptionForceAlwaysRelay) { option == kOptionForceAlwaysRelay) {
res = b ? 'Y' : ''; res = b ? 'Y' : defaultOptionNo;
} else { } else {
assert(false); assert(false);
res = b ? 'Y' : 'N'; res = b ? 'Y' : 'N';
@@ -1455,9 +1503,9 @@ bool mainGetPeerBoolOptionSync(String id, String key) {
return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key)); return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
} }
mainSetPeerBoolOptionSync(String id, String key, bool v) { // Don't use `option2bool()` and `bool2option()` to convert the session option.
bind.mainSetPeerOptionSync(id: id, key: key, value: bool2option(key, v)); // Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
} // Because all session options use `Y` and `<Empty>` as values.
Future<bool> matchPeer(String searchText, Peer peer) async { Future<bool> matchPeer(String searchText, Peer peer) async {
if (searchText.isEmpty) { if (searchText.isEmpty) {
@@ -1493,6 +1541,12 @@ Widget getPlatformImage(String platform, {double size = 50}) {
return SvgPicture.asset('assets/$platform.svg', height: size, width: size); return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
} }
class OffsetDevicePixelRatio {
Offset offset;
final double devicePixelRatio;
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
}
class LastWindowPosition { class LastWindowPosition {
double? width; double? width;
double? height; double? height;
@@ -1554,16 +1608,13 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
late Offset position; late Offset position;
late Size sz; late Size sz;
late bool isMaximized; late bool isMaximized;
bool isFullscreen = stateGlobal.fullscreen.isTrue || bool isFullscreen = stateGlobal.fullscreen.isTrue;
(isMacOS && stateGlobal.closeOnFullscreen == true); setPreFrame() {
setFrameIfMaximized() { final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
if (isMaximized) { var lpos = LastWindowPosition.loadFromString(pos);
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name); position = Offset(
var lpos = LastWindowPosition.loadFromString(pos); lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
position = Offset( sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
}
} }
switch (type) { switch (type) {
@@ -1572,26 +1623,33 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
// `await windowManager.isMaximized()` will always return true // `await windowManager.isMaximized()` will always return true
// if is not resizable. The reason is unknown. // if is not resizable. The reason is unknown.
// //
// `windowManager.setResizable(!bind.isIncomingOnly());` in main.dart // `setResizable(!bind.isIncomingOnly());` in main.dart
isMaximized = isMaximized =
bind.isIncomingOnly() ? false : await windowManager.isMaximized(); bind.isIncomingOnly() ? false : await windowManager.isMaximized();
position = await windowManager.getPosition(); if (isFullscreen || isMaximized) {
sz = await windowManager.getSize(); setPreFrame();
setFrameIfMaximized(); } else {
position = await windowManager.getPosition();
sz = await windowManager.getSize();
}
break; break;
default: default:
final wc = WindowController.fromWindowId(windowId!); final wc = WindowController.fromWindowId(windowId!);
isMaximized = await wc.isMaximized(); isMaximized = await wc.isMaximized();
final Rect frame; if (isFullscreen || isMaximized) {
try { setPreFrame();
frame = await wc.getFrame(); } else {
} catch (e) { final Rect frame;
debugPrint("Failed to get frame of window $windowId, it may be hidden"); try {
return; frame = await wc.getFrame();
} catch (e) {
debugPrint(
"Failed to get frame of window $windowId, it may be hidden");
return;
}
position = frame.topLeft;
sz = frame.size;
} }
position = frame.topLeft;
sz = frame.size;
setFrameIfMaximized();
break; break;
} }
if (isWindows) { if (isWindows) {
@@ -1625,7 +1683,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
final remoteList = await DesktopMultiWindow.invokeMethod( final remoteList = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventGetRemoteList, null); windowId, kWindowEventGetRemoteList, null);
getPeerPos(String peerId) { getPeerPos(String peerId) {
if (isMaximized) { if (isMaximized || isFullscreen) {
final peerPos = bind.mainGetPeerFlutterOptionSync( final peerPos = bind.mainGetPeerFlutterOptionSync(
id: peerId, k: windowFramePrefix + windowType.name); id: peerId, k: windowFramePrefix + windowType.name);
var lpos = LastWindowPosition.loadFromString(peerPos); var lpos = LastWindowPosition.loadFromString(peerPos);
@@ -1682,8 +1740,15 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
return Size(restoreWidth, restoreHeight); return Size(restoreWidth, restoreHeight);
} }
bool isPointInRect(Offset point, Rect rect) {
return point.dx >= rect.left &&
point.dx <= rect.right &&
point.dy >= rect.top &&
point.dy <= rect.bottom;
}
/// return null means center /// return null means center
Future<Offset?> _adjustRestoreMainWindowOffset( Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
double? left, double? left,
double? top, double? top,
double? width, double? width,
@@ -1697,9 +1762,13 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
double? frameTop; double? frameTop;
double? frameRight; double? frameRight;
double? frameBottom; double? frameBottom;
double devicePixelRatio = 1.0;
if (isDesktop || isWebDesktop) { if (isDesktop || isWebDesktop) {
for (final screen in await window_size.getScreenList()) { for (final screen in await window_size.getScreenList()) {
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
devicePixelRatio = screen.scaleFactor;
}
frameLeft = frameLeft == null frameLeft = frameLeft == null
? screen.visibleFrame.left ? screen.visibleFrame.left
: min(screen.visibleFrame.left, frameLeft); : min(screen.visibleFrame.left, frameLeft);
@@ -1733,7 +1802,7 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
top < frameTop!) { top < frameTop!) {
return null; return null;
} else { } else {
return Offset(left, top); return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
} }
} }
@@ -1759,15 +1828,10 @@ Future<bool> restoreWindowPosition(WindowType type,
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
// Though "open in tabs" is true and the new window restore peer position, it's ok. // Though "open in tabs" is true and the new window restore peer position, it's ok.
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) { if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
// If the restore position is called by main window, and the peer id is not null final peerPos = bind.mainGetPeerFlutterOptionSync(
// then we may need to get the position by reading the peer config. id: peerId, k: windowFramePrefix + type.name);
// Because the session may not be read at this time. if (peerPos.isNotEmpty) {
if (desktopType == DesktopType.main) { pos = peerPos;
pos = bind.mainGetPeerFlutterOptionSync(
id: peerId, k: windowFramePrefix + type.name);
} else {
pos = await bind.sessionGetFlutterOptionByPeerId(
id: peerId, k: windowFramePrefix + type.name);
} }
isRemotePeerPos = pos != null; isRemotePeerPos = pos != null;
} }
@@ -1798,22 +1862,47 @@ Future<bool> restoreWindowPosition(WindowType type,
} }
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height); final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
final offset = await _adjustRestoreMainWindowOffset( final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
lpos.offsetWidth, lpos.offsetWidth,
lpos.offsetHeight, lpos.offsetHeight,
size.width, size.width,
size.height, size.height,
); );
debugPrint( debugPrint(
"restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}"); "restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
switch (type) { switch (type) {
case WindowType.Main: case WindowType.Main:
// https://github.com/rustdesk/rustdesk/issues/8038
// `setBounds()` in `window_manager` will use the current devicePixelRatio.
// So we need to adjust the offset by the scale factor.
// https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
if (isWindows) {
double? curDevicePixelRatio;
Offset curPos = await windowManager.getPosition();
for (final screen in await window_size.getScreenList()) {
if (isPointInRect(curPos, screen.visibleFrame)) {
curDevicePixelRatio = screen.scaleFactor;
}
}
if (curDevicePixelRatio != null &&
curDevicePixelRatio != 0 &&
offsetDevicePixelRatio != null) {
if (offsetDevicePixelRatio.devicePixelRatio != 0) {
final scale =
offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
offsetDevicePixelRatio.offset =
offsetDevicePixelRatio.offset.scale(scale, scale);
debugPrint(
"restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
}
}
}
restorePos() async { restorePos() async {
if (offset == null) { if (offsetDevicePixelRatio == null) {
await windowManager.center(); await windowManager.center();
} else { } else {
await windowManager.setPosition(offset); await windowManager.setPosition(offsetDevicePixelRatio.offset);
} }
} }
if (lpos.isMaximized == true) { if (lpos.isMaximized == true) {
@@ -1831,19 +1920,27 @@ Future<bool> restoreWindowPosition(WindowType type,
default: default:
final wc = WindowController.fromWindowId(windowId!); final wc = WindowController.fromWindowId(windowId!);
restoreFrame() async { restoreFrame() async {
if (offset == null) { if (offsetDevicePixelRatio == null) {
await wc.center(); await wc.center();
} else { } else {
final frame = final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height); offsetDevicePixelRatio.offset.dy, size.width, size.height);
await wc.setFrame(frame); await wc.setFrame(frame);
} }
} }
if (lpos.isFullscreen == true) { if (lpos.isFullscreen == true) {
await restoreFrame(); if (!isMacOS) {
await restoreFrame();
}
// An duration is needed to avoid the window being restored after fullscreen. // An duration is needed to avoid the window being restored after fullscreen.
Future.delayed(Duration(milliseconds: 300), () async { Future.delayed(Duration(milliseconds: 300), () async {
stateGlobal.setFullscreen(true); if (kWindowId == windowId) {
stateGlobal.setFullscreen(true);
} else {
// If is not current window, we need to send a fullscreen message to `windowId`
DesktopMultiWindow.invokeMethod(
windowId, kWindowEventSetFullscreen, 'true');
}
}); });
} else if (lpos.isMaximized == true) { } else if (lpos.isMaximized == true) {
await restoreFrame(); await restoreFrame();
@@ -2309,7 +2406,13 @@ Future<void> onActiveWindowChanged() async {
await windowManager.setPreventClose(false); await windowManager.setPreventClose(false);
await windowManager.close(); await windowManager.close();
if (isMacOS) { if (isMacOS) {
RdPlatformChannel.instance.terminate(); // If we call without delay, `flutter/macos/Runner/MainFlutterWindow.swift` can handle the "terminate" event.
// But the app will not close.
//
// No idea why we need to delay here, `terminate()` itself is also an async function.
Future.delayed(Duration.zero, () {
RdPlatformChannel.instance.terminate();
});
} }
} }
} }
@@ -2587,27 +2690,40 @@ Future<void> start_service(bool is_start) async {
!isMacOS || !isMacOS ||
await callMainCheckSuperUserPermission(); await callMainCheckSuperUserPermission();
if (checked) { if (checked) {
bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y"); mainSetBoolOption(kOptionStopService, !is_start);
} }
} }
Future<bool> canBeBlocked() async {
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
var option = option2bool(kOptionAllowRemoteConfigModification,
await bind.mainGetOption(key: kOptionAllowRemoteConfigModification));
return access_mode == 'view' || (access_mode.isEmpty && !option);
}
Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
if (use != null && !await use()) {
block.value = false;
return;
}
var time0 = DateTime.now().millisecondsSinceEpoch;
await bind.mainCheckMouseTime();
Timer(const Duration(milliseconds: 120), () async {
var d = time0 - await bind.mainGetMouseTime();
if (d < 120) {
block.value = true;
} else {
block.value = false;
}
});
}
typedef WhetherUseRemoteBlock = Future<bool> Function(); typedef WhetherUseRemoteBlock = Future<bool> Function();
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) { Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
var block = false.obs; var block = false.obs;
return Obx(() => MouseRegion( return Obx(() => MouseRegion(
onEnter: (_) async { onEnter: (_) async {
if (use != null && !await use()) { await shouldBeBlocked(block, use);
block.value = false;
return;
}
var time0 = DateTime.now().millisecondsSinceEpoch;
await bind.mainCheckMouseTime();
Timer(const Duration(milliseconds: 120), () async {
var d = time0 - await bind.mainGetMouseTime();
if (d < 120) {
block.value = true;
}
});
}, },
onExit: (event) => block.value = false, onExit: (event) => block.value = false,
child: Stack(children: [ child: Stack(children: [
@@ -2664,12 +2780,10 @@ Widget buildErrorBanner(BuildContext context,
required RxString err, required RxString err,
required Function? retry, required Function? retry,
required Function close}) { required Function close}) {
const double height = 25;
return Obx(() => Offstage( return Obx(() => Offstage(
offstage: !(!loading.value && err.value.isNotEmpty), offstage: !(!loading.value && err.value.isNotEmpty),
child: Center( child: Center(
child: Container( child: Container(
height: height,
color: MyTheme.color(context).errorBannerBg, color: MyTheme.color(context).errorBannerBg,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -2688,7 +2802,6 @@ Widget buildErrorBanner(BuildContext context,
message: translate(err.value), message: translate(err.value),
child: Text( child: Text(
translate(err.value), translate(err.value),
overflow: TextOverflow.ellipsis,
), ),
)).marginSymmetric(vertical: 2), )).marginSymmetric(vertical: 2),
), ),
@@ -2743,11 +2856,6 @@ sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
} }
} }
bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
pi.isSupportMultiDisplay &&
useTextureRender &&
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';
Future<List<Rect>> getScreenListWayland() async { Future<List<Rect>> getScreenListWayland() async {
final screenRectList = <Rect>[]; final screenRectList = <Rect>[];
if (isMainDesktopWindow) { if (isMainDesktopWindow) {
@@ -2846,6 +2954,16 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args)); kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
} }
setNewConnectWindowFrame(
int windowId, String peerId, int? display, Rect? screenRect) async {
if (screenRect == null) {
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: windowId, display: display, peerId: peerId);
} else {
await tryMoveToScreenAndSetFullscreen(screenRect);
}
}
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async { tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
if (screenRect == null) { if (screenRect == null) {
return; return;
@@ -2978,16 +3096,16 @@ Future<bool> setServerConfig(
} }
// id // id
if (config.idServer.isNotEmpty && errMsgs != null) { if (config.idServer.isNotEmpty && errMsgs != null) {
errMsgs[0].value = errMsgs[0].value = translate(await bind.mainTestIfValidServer(
translate(await bind.mainTestIfValidServer(server: config.idServer)); server: config.idServer, testWithProxy: true));
if (errMsgs[0].isNotEmpty) { if (errMsgs[0].isNotEmpty) {
return false; return false;
} }
} }
// relay // relay
if (config.relayServer.isNotEmpty && errMsgs != null) { if (config.relayServer.isNotEmpty && errMsgs != null) {
errMsgs[1].value = errMsgs[1].value = translate(await bind.mainTestIfValidServer(
translate(await bind.mainTestIfValidServer(server: config.relayServer)); server: config.relayServer, testWithProxy: true));
if (errMsgs[1].isNotEmpty) { if (errMsgs[1].isNotEmpty) {
return false; return false;
} }
@@ -3106,6 +3224,27 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6); : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
} }
Widget loadPowered(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
launchUrl(Uri.parse('https://rustdesk.com'));
},
child: Opacity(
opacity: 0.5,
child: Text(
translate("powered_by_me"),
overflow: TextOverflow.clip,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(fontSize: 9, decoration: TextDecoration.underline),
)),
),
).marginOnly(top: 6);
}
// max 300 x 60 // max 300 x 60
Widget loadLogo() { Widget loadLogo() {
return FutureBuilder<ByteData>( return FutureBuilder<ByteData>(
@@ -3155,3 +3294,138 @@ bool isInHomePage() {
final controller = Get.find<DesktopTabController>(); final controller = Get.find<DesktopTabController>();
return controller.state.value.selected == 0; return controller.state.value.selected == 0;
} }
Widget buildPresetPasswordWarning() {
return FutureBuilder<bool>(
future: bind.isPresetPassword(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
} else if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
} else if (snapshot.hasData && snapshot.data == true) {
return 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 {
return SizedBox
.shrink(); // Show nothing if the Future completed with false or null
}
},
);
}
// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
boxShadow() => isMainDesktopWindow
? <BoxShadow>[
if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse)
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: Offset(
0.0,
stateGlobal.isFocused.isTrue
? kFrameBoxShadowOffsetFocused
: kFrameBoxShadowOffsetUnfocused),
blurRadius: kFrameBoxShadowBlurRadius,
),
]
: null;
return Obx(
() => Container(
decoration: BoxDecoration(
color: isMainDesktopWindow
? Colors.transparent
: Theme.of(context).colorScheme.background,
border: Border.all(
color: Theme.of(context).dividerColor,
width: stateGlobal.windowBorderWidth.value,
),
borderRadius: BorderRadius.circular(
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
? 0
: kFrameBorderRadius,
),
boxShadow: boxShadow(),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
? 0
: kFrameClipRRectBorderRadius,
),
child: child,
),
),
);
}
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
// See _linuxWindowResizable for more details.
// So we use `setResizable()` instead of `windowManager.setResizable()`.
//
// We can only call `windowManager.setResizable(false)` if we need the default size on Linux.
setResizable(bool resizable) {
if (isLinux) {
_linuxWindowResizable = resizable;
stateGlobal.refreshResizeEdgeSize();
} else {
windowManager.setResizable(resizable);
}
}
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
final isCustomClient = bind.isCustomClient();
get defaultOptionLang => isCustomClient ? 'default' : '';
get defaultOptionTheme => isCustomClient ? 'system' : '';
get defaultOptionYes => isCustomClient ? 'Y' : '';
get defaultOptionNo => isCustomClient ? 'N' : '';
get defaultOptionWhitelist => isCustomClient ? ',' : '';
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
// `setMovable()` is only supported on macOS.
//
// On macOS, the window can be dragged by the tab bar by default.
// We need to disable the movable feature to prevent the window from being dragged by the tabs in the tab bar.
//
// When we drag the blank tab bar (not the tab), the window will be dragged normally by adding the `onPanStart` handle.
//
// See the following code for more details:
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L385
// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L399
//
// @platforms macos
disableWindowMovable(int? windowId) {
if (!isMacOS) {
return;
}
if (windowId == null) {
windowManager.setMovable(false);
} else {
WindowController.fromWindowId(windowId).setMovable(false);
}
}

View File

@@ -168,6 +168,29 @@ class ShowRemoteCursorState {
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id)); static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
} }
class ShowRemoteCursorLockState {
static String tag(String id) => 'show_remote_cursor_lock_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
final RxBool state = false.obs;
Get.put(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class KeyboardEnabledState { class KeyboardEnabledState {
static String tag(String id) => 'keyboard_enabled_$id'; static String tag(String id) => 'keyboard_enabled_$id';
@@ -315,9 +338,10 @@ initSharedStates(String id) {
CurrentDisplayState.init(id); CurrentDisplayState.init(id);
KeyboardEnabledState.init(id); KeyboardEnabledState.init(id);
ShowRemoteCursorState.init(id); ShowRemoteCursorState.init(id);
ShowRemoteCursorLockState.init(id);
RemoteCursorMovedState.init(id); RemoteCursorMovedState.init(id);
FingerprintState.init(id); FingerprintState.init(id);
PeerBoolOption.init(id, 'zoom-cursor', () => false); PeerBoolOption.init(id, kOptionZoomCursor, () => false);
UnreadChatCountState.init(id); UnreadChatCountState.init(id);
if (isMobile) ConnectionTypeState.init(id); // desktop in other places if (isMobile) ConnectionTypeState.init(id); // desktop in other places
} }
@@ -327,10 +351,11 @@ removeSharedStates(String id) {
BlockInputState.delete(id); BlockInputState.delete(id);
CurrentDisplayState.delete(id); CurrentDisplayState.delete(id);
ShowRemoteCursorState.delete(id); ShowRemoteCursorState.delete(id);
ShowRemoteCursorLockState.delete(id);
KeyboardEnabledState.delete(id); KeyboardEnabledState.delete(id);
RemoteCursorMovedState.delete(id); RemoteCursorMovedState.delete(id);
FingerprintState.delete(id); FingerprintState.delete(id);
PeerBoolOption.delete(id, 'zoom-cursor'); PeerBoolOption.delete(id, kOptionZoomCursor);
UnreadChatCountState.delete(id); UnreadChatCountState.delete(id);
if (isMobile) ConnectionTypeState.delete(id); if (isMobile) ConnectionTypeState.delete(id);
} }

View File

@@ -7,6 +7,7 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
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';
@@ -109,6 +110,7 @@ class _AddressBookState extends State<AddressBook> {
} }
Widget _buildAddressBookMobile() { Widget _buildAddressBookMobile() {
const padding = 8.0;
return Column( return Column(
children: [ children: [
Offstage( Offstage(
@@ -119,7 +121,8 @@ class _AddressBookState extends State<AddressBook> {
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.background)), color: Theme.of(context).colorScheme.background)),
child: Container( child: Container(
padding: const EdgeInsets.all(8.0), padding:
const EdgeInsets.fromLTRB(padding, 0, padding, padding),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -129,7 +132,6 @@ class _AddressBookState extends State<AddressBook> {
width: double.infinity, width: double.infinity,
child: _buildTags(), child: _buildTags(),
), ),
_buildAbPermission(),
], ],
), ),
), ),
@@ -189,42 +191,73 @@ class _AddressBookState extends State<AddressBook> {
if (!names.contains(gFFI.abModel.currentName.value)) { if (!names.contains(gFFI.abModel.currentName.value)) {
return Offstage(); return Offstage();
} }
// order: personal, divider, character order
// https://pub.dev/packages/dropdown_button2#3-dropdownbutton2-with-items-of-different-heights-like-dividers
final personalAddressBookName = gFFI.abModel.personalAddressBookName();
bool contains = names.remove(personalAddressBookName);
names.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
if (contains) {
names.insert(0, personalAddressBookName);
}
Row buildItem(String e, {bool button = false}) {
return Row(
children: [
Expanded(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: gFFI.abModel.translatedName(e),
child: Text(
gFFI.abModel.translatedName(e),
style: button ? null : TextStyle(fontSize: 14.0),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: button ? TextAlign.center : null,
)),
),
],
);
}
final items = names
.map((e) => DropdownMenuItem(value: e, child: buildItem(e)))
.toList();
var menuItemStyleData = MenuItemStyleData(height: 36);
if (contains && items.length > 1) {
items.insert(1, DropdownMenuItem(enabled: false, child: Divider()));
List<double> customHeights = List.filled(items.length, 36);
customHeights[1] = 4;
menuItemStyleData = MenuItemStyleData(customHeights: customHeights);
}
final TextEditingController textEditingController = TextEditingController(); final TextEditingController textEditingController = TextEditingController();
final isOptFixed = isOptionFixed(kOptionCurrentAbName);
return DropdownButton2<String>( return DropdownButton2<String>(
value: gFFI.abModel.currentName.value, value: gFFI.abModel.currentName.value,
onChanged: (value) { onChanged: isOptFixed
if (value != null) { ? null
gFFI.abModel.setCurrentName(value); : (value) {
bind.setLocalFlutterOption(k: 'current-ab-name', v: value); if (value != null) {
} gFFI.abModel.setCurrentName(value);
}, bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
}
},
customButton: Container(
height: isDesktop ? 48 : 40,
child: Row(children: [
Expanded(
child: buildItem(gFFI.abModel.currentName.value, button: true)),
Icon(Icons.arrow_drop_down),
]),
),
underline: Container( underline: Container(
height: 0.7, height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1), color: Theme.of(context).dividerColor.withOpacity(0.1),
), ),
buttonStyleData: ButtonStyleData(height: 48), menuItemStyleData: menuItemStyleData,
menuItemStyleData: MenuItemStyleData(height: 36), items: items,
items: names
.map((e) => DropdownMenuItem(
value: e,
child: Row(
children: [
Expanded(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: gFFI.abModel.translatedName(e),
child: Text(
gFFI.abModel.translatedName(e),
style: TextStyle(fontSize: 14.0),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
),
],
)))
.toList(),
isExpanded: true, isExpanded: true,
isDense: true,
dropdownSearchData: DropdownSearchData( dropdownSearchData: DropdownSearchData(
searchController: textEditingController, searchController: textEditingController,
searchInnerWidgetHeight: 50, searchInnerWidgetHeight: 50,
@@ -333,6 +366,7 @@ class _AddressBookState extends State<AddressBook> {
@protected @protected
MenuEntryBase<String> syncMenuItem() { MenuEntryBase<String> syncMenuItem() {
final isOptFixed = isOptionFixed(syncAbOption);
return MenuEntrySwitch<String>( return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: translate('Sync with recent sessions'), text: translate('Sync with recent sessions'),
@@ -343,11 +377,13 @@ class _AddressBookState extends State<AddressBook> {
gFFI.abModel.setShouldAsync(v); gFFI.abModel.setShouldAsync(v);
}, },
dismissOnClicked: true, dismissOnClicked: true,
enabled: (!isOptFixed).obs,
); );
} }
@protected @protected
MenuEntryBase<String> sortMenuItem() { MenuEntryBase<String> sortMenuItem() {
final isOptFixed = isOptionFixed(sortAbTagsOption);
return MenuEntrySwitch<String>( return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: translate('Sort tags'), text: translate('Sort tags'),
@@ -355,15 +391,18 @@ class _AddressBookState extends State<AddressBook> {
return shouldSortTags(); return shouldSortTags();
}, },
setter: (bool v) async { setter: (bool v) async {
bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : ''); bind.mainSetLocalOption(
key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo);
gFFI.abModel.sortTags.value = v; gFFI.abModel.sortTags.value = v;
}, },
dismissOnClicked: true, dismissOnClicked: true,
enabled: (!isOptFixed).obs,
); );
} }
@protected @protected
MenuEntryBase<String> filterMenuItem() { MenuEntryBase<String> filterMenuItem() {
final isOptFixed = isOptionFixed(filterAbTagOption);
return MenuEntrySwitch<String>( return MenuEntrySwitch<String>(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: translate('Filter by intersection'), text: translate('Filter by intersection'),
@@ -371,10 +410,12 @@ class _AddressBookState extends State<AddressBook> {
return filterAbTagByIntersection(); return filterAbTagByIntersection();
}, },
setter: (bool v) async { setter: (bool v) async {
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : ''); bind.mainSetLocalOption(
key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo);
gFFI.abModel.filterByIntersection.value = v; gFFI.abModel.filterByIntersection.value = v;
}, },
dismissOnClicked: true, dismissOnClicked: true,
enabled: (!isOptFixed).obs,
); );
} }
@@ -385,10 +426,11 @@ class _AddressBookState extends State<AddressBook> {
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(), sortMenuItem(),
syncMenuItem(), if (canWrite) syncMenuItem(),
filterMenuItem(), filterMenuItem(),
if (!gFFI.abModel.legacyMode.value) MenuEntryDivider<String>(), if (!gFFI.abModel.legacyMode.value && canWrite)
if (!gFFI.abModel.legacyMode.value) MenuEntryDivider<String>(),
if (!gFFI.abModel.legacyMode.value && canWrite)
getEntry(translate("ab_web_console_tip"), () async { getEntry(translate("ab_web_console_tip"), () async {
final url = await bind.mainGetApiServer(); final url = await bind.mainGetApiServer();
if (await canLaunchUrlString(url)) { if (await canLaunchUrlString(url)) {

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function(
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
class AudioInput extends StatelessWidget {
final AudioInputBuilder builder;
const AudioInput({Key? key, required this.builder}) : super(key: key);
static String getDefault() {
if (isWindows) return translate('System Sound');
return '';
}
static Future<String> getValue() async {
String device = await bind.mainGetOption(key: 'audio-input');
if (device.isNotEmpty) {
return device;
} else {
return getDefault();
}
}
static Future<void> setDevice(String device) async {
if (device == getDefault()) device = '';
await bind.mainSetOption(key: 'audio-input', value: device);
}
static Future<Map<String, Object>> getDevicesInfo() async {
List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) {
devices.insert(0, translate('System Sound'));
}
String current = await getValue();
return {'devices': devices, 'current': current};
}
@override
Widget build(BuildContext context) {
return futureBuilder(
future: getDevicesInfo(),
hasData: (data) {
String currentDevice = data['current'];
List<String> devices = data['devices'] as List<String>;
if (devices.isEmpty) {
return const Offstage();
}
return builder(devices, currentDevice, setDevice);
},
);
}
}

View File

@@ -177,11 +177,14 @@ void changeIdDialog() {
} }
void changeWhiteList({Function()? callback}) async { void changeWhiteList({Function()? callback}) async {
var newWhiteList = (await bind.mainGetOption(key: 'whitelist')).split(','); final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist);
var newWhiteListField = newWhiteList.join('\n'); var newWhiteListField = curWhiteList == defaultOptionWhitelist
? ''
: curWhiteList.split(',').join('\n');
var controller = TextEditingController(text: newWhiteListField); var controller = TextEditingController(text: newWhiteListField);
var msg = ""; var msg = "";
var isInProgress = false; var isInProgress = false;
final isOptFixed = isOptionFixed(kOptionWhitelist);
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate("IP Whitelisting")), title: Text(translate("IP Whitelisting")),
@@ -201,6 +204,7 @@ void changeWhiteList({Function()? callback}) async {
errorText: msg.isEmpty ? null : translate(msg), errorText: msg.isEmpty ? null : translate(msg),
), ),
controller: controller, controller: controller,
enabled: !isOptFixed,
autofocus: true), autofocus: true),
), ),
], ],
@@ -214,12 +218,13 @@ void changeWhiteList({Function()? callback}) async {
), ),
actions: [ actions: [
dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("Clear", onPressed: () async { if (!isOptFixed)dialogButton("Clear", onPressed: () async {
await bind.mainSetOption(key: 'whitelist', value: ''); await bind.mainSetOption(
key: kOptionWhitelist, value: defaultOptionWhitelist);
callback?.call(); callback?.call();
close(); close();
}, isOutline: true), }, isOutline: true),
dialogButton( if (!isOptFixed) dialogButton(
"OK", "OK",
onPressed: () async { onPressed: () async {
setState(() { setState(() {
@@ -248,7 +253,11 @@ void changeWhiteList({Function()? callback}) async {
} }
newWhiteList = ips.join(','); newWhiteList = ips.join(',');
} }
await bind.mainSetOption(key: 'whitelist', value: newWhiteList); if (newWhiteList.trim().isEmpty) {
newWhiteList = defaultOptionWhitelist;
}
await bind.mainSetOption(
key: kOptionWhitelist, value: newWhiteList);
callback?.call(); callback?.call();
close(); close();
}, },
@@ -298,7 +307,7 @@ Future<String> changeDirectAccessPort(
dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: () async { dialogButton("OK", onPressed: () async {
await bind.mainSetOption( await bind.mainSetOption(
key: 'direct-access-port', value: controller.text); key: kOptionDirectAccessPort, value: controller.text);
close(); close();
}), }),
], ],
@@ -345,7 +354,7 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: () async { dialogButton("OK", onPressed: () async {
await bind.mainSetOption( await bind.mainSetOption(
key: 'auto-disconnect-timeout', value: controller.text); key: kOptionAutoDisconnectTimeout, value: controller.text);
close(); close();
}), }),
], ],

View File

@@ -112,6 +112,8 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
}; };
} }
// FIXME: This debounce logic is not working properly.
// If we move our finger very fast, we won't be able to detect the "oneFingerPan" event sometimes.
void onOneFingerStartDebounce(ScaleUpdateDetails d) { void onOneFingerStartDebounce(ScaleUpdateDetails d) {
start(ScaleUpdateDetails d) { start(ScaleUpdateDetails d) {
_currentState = GestureState.oneFingerPan; _currentState = GestureState.oneFingerPan;

View File

@@ -455,7 +455,7 @@ Future<bool?> loginDialog() async {
} }
if (isEmailVerification != null) { if (isEmailVerification != null) {
if (isMobile) { if (isMobile) {
if (close != null) close(false); if (close != null) close(null);
verificationCodeDialog( verificationCodeDialog(
resp.user, resp.secret, isEmailVerification); resp.user, resp.secret, isEmailVerification);
} else { } else {
@@ -712,6 +712,11 @@ Future<bool?> verificationCodeDialog(
dialogButton("Verify", onPressed: getOnSubmit()), dialogButton("Verify", onPressed: getOnSubmit()),
]); ]);
}); });
// For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
// otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
if (isMobile && res == true) {
await UserModel.updateOtherModels();
}
return res; return res;
} }

View File

@@ -1,6 +1,8 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (draggablePositions.chatWindow.isInvalid()) {
draggablePositions.chatWindow.update(position);
}
return isIOS return isIOS
? IOSDraggable( ? IOSDraggable(
position: position, position: draggablePositions.chatWindow,
chatModel: chatModel, chatModel: chatModel,
width: width, width: width,
height: height, height: height,
@@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget {
) )
: Draggable( : Draggable(
checkKeyboard: true, checkKeyboard: true,
position: position, position: draggablePositions.chatWindow,
width: width, width: width,
height: height, height: height,
chatModel: chatModel, chatModel: chatModel,
@@ -166,15 +171,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
/// floating buttons of back/home/recent actions for android /// floating buttons of back/home/recent actions for android
class DraggableMobileActions extends StatelessWidget { class DraggableMobileActions extends StatelessWidget {
DraggableMobileActions( DraggableMobileActions(
{this.position = Offset.zero, {this.onBackPressed,
this.onBackPressed,
this.onRecentPressed, this.onRecentPressed,
this.onHomePressed, this.onHomePressed,
this.onHidePressed, this.onHidePressed,
required this.position,
required this.width, required this.width,
required this.height}); required this.height,
required this.scale});
final Offset position; final double scale;
final DraggableKeyPosition position;
final double width; final double width;
final double height; final double height;
final VoidCallback? onBackPressed; final VoidCallback? onBackPressed;
@@ -186,8 +193,8 @@ class DraggableMobileActions extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Draggable( return Draggable(
position: position, position: position,
width: width, width: scale * width,
height: height, height: scale * height,
builder: (_, onPanUpdate) { builder: (_, onPanUpdate) {
return GestureDetector( return GestureDetector(
onPanUpdate: onPanUpdate, onPanUpdate: onPanUpdate,
@@ -197,7 +204,8 @@ class DraggableMobileActions extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: MyTheme.accent.withOpacity(0.4), color: MyTheme.accent.withOpacity(0.4),
borderRadius: BorderRadius.all(Radius.circular(15))), borderRadius:
BorderRadius.all(Radius.circular(15 * scale))),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
@@ -205,17 +213,20 @@ class DraggableMobileActions extends StatelessWidget {
color: Colors.white, color: Colors.white,
onPressed: onBackPressed, onPressed: onBackPressed,
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.arrow_back)), icon: const Icon(Icons.arrow_back),
iconSize: 24 * scale),
IconButton( IconButton(
color: Colors.white, color: Colors.white,
onPressed: onHomePressed, onPressed: onHomePressed,
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.home)), icon: const Icon(Icons.home),
iconSize: 24 * scale),
IconButton( IconButton(
color: Colors.white, color: Colors.white,
onPressed: onRecentPressed, onPressed: onRecentPressed,
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.more_horiz)), icon: const Icon(Icons.more_horiz),
iconSize: 24 * scale),
const VerticalDivider( const VerticalDivider(
width: 0, width: 0,
thickness: 2, thickness: 2,
@@ -226,7 +237,8 @@ class DraggableMobileActions extends StatelessWidget {
color: Colors.white, color: Colors.white,
onPressed: onHidePressed, onPressed: onHidePressed,
splashRadius: kDesktopIconButtonSplashRadius, splashRadius: kDesktopIconButtonSplashRadius,
icon: const Icon(Icons.keyboard_arrow_down)), icon: const Icon(Icons.keyboard_arrow_down),
iconSize: 24 * scale),
], ],
), ),
))); )));
@@ -234,12 +246,98 @@ class DraggableMobileActions extends StatelessWidget {
} }
} }
class DraggableKeyPosition {
final String key;
Offset _pos;
late Debouncer<int> _debouncerStore;
DraggableKeyPosition(this.key)
: _pos = DraggablePositions.kInvalidDraggablePosition;
get pos => _pos;
_loadPosition(String k) {
final value = bind.getLocalFlutterOption(k: k);
if (value.isNotEmpty) {
final parts = value.split(',');
if (parts.length == 2) {
return Offset(double.parse(parts[0]), double.parse(parts[1]));
}
}
return DraggablePositions.kInvalidDraggablePosition;
}
load() {
_pos = _loadPosition(key);
_debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
onChanged: (v) => _store(), initialValue: 0);
}
update(Offset pos) {
_pos = pos;
_triggerStore();
}
// Adjust position to keep it in the screen
// Only used for desktop and web desktop
tryAdjust(double w, double h, double scale) {
final size = MediaQuery.of(Get.context!).size;
w = w * scale;
h = h * scale;
double x = _pos.dx;
double y = _pos.dy;
if (x + w > size.width) {
x = size.width - w;
}
final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
if (y + h > (size.height - tabBarHeight)) {
y = size.height - tabBarHeight - h;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (x != _pos.dx || y != _pos.dy) {
update(Offset(x, y));
}
}
isInvalid() {
return _pos == DraggablePositions.kInvalidDraggablePosition;
}
_triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
_store() {
bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
}
}
class DraggablePositions {
static const kChatWindow = 'draggablePositionChat';
static const kMobileActions = 'draggablePositionMobile';
static const kIOSDraggable = 'draggablePositionIOS';
static const kInvalidDraggablePosition = Offset(-999999, -999999);
final chatWindow = DraggableKeyPosition(kChatWindow);
final mobileActions = DraggableKeyPosition(kMobileActions);
final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
load() {
chatWindow.load();
mobileActions.load();
iOSDraggable.load();
}
}
DraggablePositions draggablePositions = DraggablePositions();
class Draggable extends StatefulWidget { class Draggable extends StatefulWidget {
const Draggable( Draggable(
{Key? key, {Key? key,
this.checkKeyboard = false, this.checkKeyboard = false,
this.checkScreenSize = false, this.checkScreenSize = false,
this.position = Offset.zero, required this.position,
required this.width, required this.width,
required this.height, required this.height,
this.chatModel, this.chatModel,
@@ -248,7 +346,7 @@ class Draggable extends StatefulWidget {
final bool checkKeyboard; final bool checkKeyboard;
final bool checkScreenSize; final bool checkScreenSize;
final Offset position; final DraggableKeyPosition position;
final double width; final double width;
final double height; final double height;
final ChatModel? chatModel; final ChatModel? chatModel;
@@ -259,7 +357,6 @@ class Draggable extends StatefulWidget {
} }
class _DraggableState extends State<Draggable> { class _DraggableState extends State<Draggable> {
late Offset _position;
late ChatModel? _chatModel; late ChatModel? _chatModel;
bool _keyboardVisible = false; bool _keyboardVisible = false;
double _saveHeight = 0; double _saveHeight = 0;
@@ -268,35 +365,36 @@ class _DraggableState extends State<Draggable> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_position = widget.position;
_chatModel = widget.chatModel; _chatModel = widget.chatModel;
} }
get position => widget.position.pos;
void onPanUpdate(DragUpdateDetails d) { void onPanUpdate(DragUpdateDetails d) {
final offset = d.delta; final offset = d.delta;
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
double x = 0; double x = 0;
double y = 0; double y = 0;
if (_position.dx + offset.dx + widget.width > size.width) { if (position.dx + offset.dx + widget.width > size.width) {
x = size.width - widget.width; x = size.width - widget.width;
} else if (_position.dx + offset.dx < 0) { } else if (position.dx + offset.dx < 0) {
x = 0; x = 0;
} else { } else {
x = _position.dx + offset.dx; x = position.dx + offset.dx;
} }
if (_position.dy + offset.dy + widget.height > size.height) { if (position.dy + offset.dy + widget.height > size.height) {
y = size.height - widget.height; y = size.height - widget.height;
} else if (_position.dy + offset.dy < 0) { } else if (position.dy + offset.dy < 0) {
y = 0; y = 0;
} else { } else {
y = _position.dy + offset.dy; y = position.dy + offset.dy;
} }
setState(() { setState(() {
_position = Offset(x, y); widget.position.update(Offset(x, y));
}); });
_chatModel?.setChatWindowPosition(_position); _chatModel?.setChatWindowPosition(position);
} }
checkScreenSize() {} checkScreenSize() {}
@@ -307,13 +405,13 @@ class _DraggableState extends State<Draggable> {
// save // save
if (!_keyboardVisible && currentVisible) { if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy; _saveHeight = position.dy;
} }
// reset // reset
if (_lastBottomHeight > 0 && bottomHeight == 0) { if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() { setState(() {
_position = Offset(_position.dx, _saveHeight); widget.position.update(Offset(position.dx, _saveHeight));
}); });
} }
@@ -321,10 +419,10 @@ class _DraggableState extends State<Draggable> {
if (_keyboardVisible && currentVisible) { if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + widget.height; final sumHeight = bottomHeight + widget.height;
final contextHeight = MediaQuery.of(context).size.height; final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) { if (sumHeight + position.dy > contextHeight) {
final y = contextHeight - sumHeight; final y = contextHeight - sumHeight;
setState(() { setState(() {
_position = Offset(_position.dx, y); widget.position.update(Offset(position.dx, y));
}); });
} }
} }
@@ -343,8 +441,8 @@ class _DraggableState extends State<Draggable> {
} }
return Stack(children: [ return Stack(children: [
Positioned( Positioned(
top: _position.dy, top: position.dy,
left: _position.dx, left: position.dx,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
child: widget.builder(context, onPanUpdate)) child: widget.builder(context, onPanUpdate))
@@ -355,14 +453,14 @@ class _DraggableState extends State<Draggable> {
class IOSDraggable extends StatefulWidget { class IOSDraggable extends StatefulWidget {
const IOSDraggable( const IOSDraggable(
{Key? key, {Key? key,
this.position = Offset.zero,
this.chatModel, this.chatModel,
required this.position,
required this.width, required this.width,
required this.height, required this.height,
required this.builder}) required this.builder})
: super(key: key); : super(key: key);
final Offset position; final DraggableKeyPosition position;
final ChatModel? chatModel; final ChatModel? chatModel;
final double width; final double width;
final double height; final double height;
@@ -373,7 +471,6 @@ class IOSDraggable extends StatefulWidget {
} }
class IOSDraggableState extends State<IOSDraggable> { class IOSDraggableState extends State<IOSDraggable> {
late Offset _position;
late ChatModel? _chatModel; late ChatModel? _chatModel;
late double _width; late double _width;
late double _height; late double _height;
@@ -384,25 +481,26 @@ class IOSDraggableState extends State<IOSDraggable> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_position = widget.position;
_chatModel = widget.chatModel; _chatModel = widget.chatModel;
_width = widget.width; _width = widget.width;
_height = widget.height; _height = widget.height;
} }
get position => widget.position;
checkKeyboard() { checkKeyboard() {
final bottomHeight = MediaQuery.of(context).viewInsets.bottom; final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
final currentVisible = bottomHeight != 0; final currentVisible = bottomHeight != 0;
// save // save
if (!_keyboardVisible && currentVisible) { if (!_keyboardVisible && currentVisible) {
_saveHeight = _position.dy; _saveHeight = position.value.dy;
} }
// reset // reset
if (_lastBottomHeight > 0 && bottomHeight == 0) { if (_lastBottomHeight > 0 && bottomHeight == 0) {
setState(() { setState(() {
_position = Offset(_position.dx, _saveHeight); position.value = Offset(position.value.dx, _saveHeight);
}); });
} }
@@ -410,10 +508,10 @@ class IOSDraggableState extends State<IOSDraggable> {
if (_keyboardVisible && currentVisible) { if (_keyboardVisible && currentVisible) {
final sumHeight = bottomHeight + _height; final sumHeight = bottomHeight + _height;
final contextHeight = MediaQuery.of(context).size.height; final contextHeight = MediaQuery.of(context).size.height;
if (sumHeight + _position.dy > contextHeight) { if (sumHeight + position.value.dy > contextHeight) {
final y = contextHeight - sumHeight; final y = contextHeight - sumHeight;
setState(() { setState(() {
_position = Offset(_position.dx, y); position.value = Offset(position.value.dx, y);
}); });
} }
} }
@@ -428,14 +526,14 @@ class IOSDraggableState extends State<IOSDraggable> {
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
left: _position.dx, left: position.value.dx,
top: _position.dy, top: position.value.dy,
child: GestureDetector( child: GestureDetector(
onPanUpdate: (details) { onPanUpdate: (details) {
setState(() { setState(() {
_position += details.delta; position.value += details.delta;
}); });
_chatModel?.setChatWindowPosition(_position); _chatModel?.setChatWindowPosition(position.value);
}, },
child: Material( child: Material(
child: Container( child: Container(
@@ -491,8 +589,10 @@ class QualityMonitor extends StatelessWidget {
children: [ children: [
_row("Speed", qualityMonitorModel.data.speed ?? '-'), _row("Speed", qualityMonitorModel.data.speed ?? '-'),
_row("FPS", qualityMonitorModel.data.fps ?? '-'), _row("FPS", qualityMonitorModel.data.fps ?? '-'),
// let delay be 0 if fps is 0
_row( _row(
"Delay", "${qualityMonitorModel.data.delay ?? '-'}ms", "Delay",
"${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
rightColor: Colors.green), rightColor: Colors.green),
_row("Target Bitrate", _row("Target Bitrate",
"${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),

View File

@@ -887,7 +887,7 @@ class RecentPeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
if (isDesktop || isWebDesktop) { if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id)); menuItems.add(_renameAction(peer.id));
} }
if (await bind.mainPeerHasPassword(id: peer.id)) { if (await bind.mainPeerHasPassword(id: peer.id)) {
@@ -943,7 +943,7 @@ class FavoritePeerCard extends BasePeerCard {
menuItems.add(_createShortCutAction(peer.id)); menuItems.add(_createShortCutAction(peer.id));
} }
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
if (isDesktop || isWebDesktop) { if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id)); menuItems.add(_renameAction(peer.id));
} }
if (await bind.mainPeerHasPassword(id: peer.id)) { if (await bind.mainPeerHasPassword(id: peer.id)) {
@@ -1048,7 +1048,7 @@ class AddressBookPeerCard extends BasePeerCard {
} }
if (gFFI.abModel.current.canWrite()) { if (gFFI.abModel.current.canWrite()) {
menuItems.add(MenuEntryDivider()); menuItems.add(MenuEntryDivider());
if (isDesktop || isWebDesktop) { if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id)); menuItems.add(_renameAction(peer.id));
} }
if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) { if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) {

View File

@@ -74,9 +74,11 @@ class _PeerTabPageState extends State<PeerTabPage>
]; ];
RelativeRect? mobileTabContextMenuPos; RelativeRect? mobileTabContextMenuPos;
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
@override @override
void initState() { void initState() {
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type'); final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
if (uiType != '') { if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == 0 peerCardUiType.value = int.parse(uiType) == 0
? PeerUiType.grid ? PeerUiType.grid
@@ -85,7 +87,7 @@ class _PeerTabPageState extends State<PeerTabPage>
: PeerUiType.list; : PeerUiType.list;
} }
hideAbTagsPanel.value = hideAbTagsPanel.value =
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty; bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
super.initState(); super.initState();
} }
@@ -173,11 +175,13 @@ class _PeerTabPageState extends State<PeerTabPage>
child: Icon(model.tabIcon(t), color: color) child: Icon(model.tabIcon(t), color: color)
.paddingSymmetric(horizontal: 4), .paddingSymmetric(horizontal: 4),
).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 4),
onTap: () async { onTap: isOptionFixed(kOptionPeerTabIndex)
await handleTabSelection(t); ? null
await bind.setLocalFlutterOption( : () async {
k: PeerTabModel.kPeerTabIndex, v: t.toString()); await handleTabSelection(t);
}, await bind.setLocalFlutterOption(
k: kOptionPeerTabIndex, v: t.toString());
},
onHover: (value) => hover.value = value, onHover: (value) => hover.value = value,
), ),
))); )));
@@ -265,17 +269,22 @@ class _PeerTabPageState extends State<PeerTabPage>
if (!model.isEnabled[i]) continue; if (!model.isEnabled[i]) continue;
items.add(PopupMenuItem( items.add(PopupMenuItem(
height: kMinInteractiveDimension * 0.8, height: kMinInteractiveDimension * 0.8,
onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]), onTap: isOptVisiableFixed
? null
: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
enabled: !isOptVisiableFixed,
child: Row( child: Row(
children: [ children: [
Checkbox( Checkbox(
value: model.isVisibleEnabled[i], value: model.isVisibleEnabled[i],
onChanged: (_) { onChanged: isOptVisiableFixed
model.setTabVisible(i, !model.isVisibleEnabled[i]); ? null
if (Navigator.canPop(context)) { : (_) {
Navigator.pop(context); model.setTabVisible(i, !model.isVisibleEnabled[i]);
} if (Navigator.canPop(context)) {
}), Navigator.pop(context);
}
}),
Expanded(child: Text(model.tabTooltip(i))), Expanded(child: Text(model.tabTooltip(i))),
], ],
), ),
@@ -332,8 +341,10 @@ class _PeerTabPageState extends State<PeerTabPage>
currentValue: model.isVisibleEnabled[tabIndex], currentValue: model.isVisibleEnabled[tabIndex],
setter: (show) async { setter: (show) async {
model.setTabVisible(tabIndex, show); model.setTabVisible(tabIndex, show);
cancelFunc(); // Do not hide the current menu (checkbox)
})); // cancelFunc();
},
enabled: (!isOptVisiableFixed).obs));
} }
return mod_menu.PopupMenu( return mod_menu.PopupMenu(
items: menu items: menu
@@ -529,7 +540,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _toggleTags() { Widget _toggleTags() {
return _hoverAction( return _hoverAction(
context: context, context: context,
toolTip: translate('Toggle tags'), toolTip: translate('Toggle Tags'),
hoverableWhenfalse: hideAbTagsPanel, hoverableWhenfalse: hideAbTagsPanel,
child: Icon( child: Icon(
Icons.tag_rounded, Icons.tag_rounded,
@@ -537,7 +548,8 @@ class _PeerTabPageState extends State<PeerTabPage>
), ),
onTap: () async { onTap: () async {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y"); key: kOptionHideAbTagsPanel,
value: hideAbTagsPanel.value ? defaultOptionNo : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value; hideAbTagsPanel.value = !hideAbTagsPanel.value;
}); });
} }
@@ -790,25 +802,38 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
child: SizedBox( child: SizedBox(
height: 36, height: 36,
child: getRadio<PeerUiType>( child: getRadio<PeerUiType>(
Text( Tooltip(
translate(types.indexOf(e) == 0 message: translate(types.indexOf(e) == 0
? 'Big tiles' ? 'Big tiles'
: types.indexOf(e) == 1 : types.indexOf(e) == 1
? 'Small tiles' ? 'Small tiles'
: 'List'), : 'List'),
style: style), child: Icon(
e == PeerUiType.grid
? Icons.grid_view_rounded
: e == PeerUiType.list
? Icons.view_list_rounded
: Icons.view_agenda_rounded,
size: 18,
)),
e, e,
peerCardUiType.value, peerCardUiType.value,
dense: true, (PeerUiType? v) async { dense: true,
if (v != null) { isOptionFixed(kOptionPeerCardUiType)
peerCardUiType.value = v; ? null
setState(() {}); : (PeerUiType? v) async {
await bind.setLocalFlutterOption( if (v != null) {
k: "peer-card-ui-type", peerCardUiType.value = v;
v: peerCardUiType.value.index.toString(), setState(() {});
); await bind.setLocalFlutterOption(
} k: kOptionPeerCardUiType,
}), v: peerCardUiType.value.index.toString(),
);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}
}),
), ),
)))); ))));
} }
@@ -820,7 +845,7 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
child: Icon( child: Icon(
peerCardUiType.value == PeerUiType.grid peerCardUiType.value == PeerUiType.grid
? Icons.grid_view_rounded ? Icons.grid_view_rounded
: peerCardUiType.value == PeerUiType.tile : peerCardUiType.value == PeerUiType.list
? Icons.view_list_rounded ? Icons.view_list_rounded
: Icons.view_agenda_rounded, : Icons.view_agenda_rounded,
size: 18, size: 18,
@@ -852,7 +877,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
if (!PeerSortType.values.contains(peerSort.value)) { if (!PeerSortType.values.contains(peerSort.value)) {
peerSort.value = PeerSortType.remoteId; peerSort.value = PeerSortType.remoteId;
bind.setLocalFlutterOption( bind.setLocalFlutterOption(
k: "peer-sorting", k: kOptionPeerSorting,
v: peerSort.value, v: peerSort.value,
); );
} }
@@ -882,7 +907,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
if (v != null) { if (v != null) {
peerSort.value = v; peerSort.value = v;
await bind.setLocalFlutterOption( await bind.setLocalFlutterOption(
k: "peer-sorting", k: kOptionPeerSorting,
v: peerSort.value, v: peerSort.value,
); );
} }

View File

@@ -4,12 +4,12 @@ import 'dart:collection';
import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:dynamic_layouts/dynamic_layouts.dart';
import 'package:flutter/foundation.dart'; 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/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.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';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import '../../common.dart'; import '../../common.dart';
import '../../models/peer_model.dart'; import '../../models/peer_model.dart';
@@ -45,7 +45,7 @@ class LoadEvent {
final peerSearchText = "".obs; final peerSearchText = "".obs;
/// for peer sort, global obs value /// for peer sort, global obs value
final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs; final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
// list for listener // list for listener
final obslist = [peerSearchText, peerSort].obs; final obslist = [peerSearchText, peerSort].obs;
@@ -86,17 +86,6 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
var _queryCount = 0; var _queryCount = 0;
var _exit = false; var _exit = false;
late final mobileWidth = () {
const minWidth = 320.0;
final windowWidth = MediaQuery.of(context).size.width;
var width = windowWidth - 2 * space;
if (windowWidth > minWidth + 2 * space) {
final n = (windowWidth / (minWidth + 2 * space)).floor();
width = windowWidth / n - 2 * space;
}
return width;
}();
final _scrollController = ScrollController(); final _scrollController = ScrollController();
_PeersViewState() { _PeersViewState() {
@@ -189,61 +178,60 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
onVisibilityChanged: onVisibilityChanged, onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer), child: widget.peerCardBuilder(peer),
); );
final windowWidth = MediaQuery.of(context).size.width;
// `Provider.of<PeerTabModel>(context)` will causes infinete loop. // `Provider.of<PeerTabModel>(context)` will causes infinete loop.
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`. // Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
// //
// 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().
final currentTab =
Provider.of<PeerTabModel>(context, listen: false).currentTab;
final hideAbTagsPanel =
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
return (isDesktop || isWebDesktop) return (isDesktop || isWebDesktop)
? Obx( ? Obx(() => peerCardUiType.value == PeerUiType.list
() => SizedBox( ? Container(height: 45, child: visibilityChild)
width: peerCardUiType.value != PeerUiType.list : peerCardUiType.value == PeerUiType.grid
? 220 ? SizedBox(
: currentTab == PeerTabIndex.group.index || width: 220, height: 140, child: visibilityChild)
(currentTab == PeerTabIndex.ab.index && : SizedBox(
!hideAbTagsPanel) width: 220, height: 42, child: visibilityChild))
? windowWidth - 390 : Container(child: visibilityChild);
: windowWidth - 227,
height: peerCardUiType.value == PeerUiType.grid
? 140
: peerCardUiType.value != PeerUiType.list
? 42
: 45,
child: visibilityChild,
),
)
: SizedBox(width: mobileWidth, child: visibilityChild);
} }
final Widget child; final Widget child;
if (isMobile) { if (isMobile) {
child = DynamicGridView.builder( child = ListView.builder(
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length, itemCount: peers.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]); return buildOnePeer(peers[index]).marginOnly(
top: index == 0 ? 0 : space / 2, bottom: space / 2);
}, },
); );
} else { } else {
child = DesktopScrollWrapper( child = Obx(() => peerCardUiType.value == PeerUiType.list
scrollController: _scrollController, ? DesktopScrollWrapper(
child: DynamicGridView.builder( scrollController: _scrollController,
controller: _scrollController, child: ListView.builder(
physics: DraggableNeverScrollableScrollPhysics(), controller: _scrollController,
gridDelegate: SliverGridDelegateWithWrapping( physics: DraggableNeverScrollableScrollPhysics(),
mainAxisSpacing: space / 2, crossAxisSpacing: space), itemCount: peers.length,
itemCount: peers.length, itemBuilder: (BuildContext context, int index) {
itemBuilder: (BuildContext context, int index) { return buildOnePeer(peers[index]).marginOnly(
return buildOnePeer(peers[index]); right: space,
}), top: index == 0 ? 0 : space / 2,
); bottom: space / 2);
}),
)
: DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2,
crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
}),
));
} }
if (updateEvent == UpdateEvent.load) { if (updateEvent == UpdateEvent.load) {
@@ -314,7 +302,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
if (!PeerSortType.values.contains(sortedBy)) { if (!PeerSortType.values.contains(sortedBy)) {
sortedBy = PeerSortType.remoteId; sortedBy = PeerSortType.remoteId;
bind.setLocalFlutterOption( bind.setLocalFlutterOption(
k: "peer-sorting", k: kOptionPeerSorting,
v: sortedBy, v: sortedBy,
); );
} }

View File

@@ -69,6 +69,8 @@ class RawTouchGestureDetectorRegion extends StatefulWidget {
class _RawTouchGestureDetectorRegionState class _RawTouchGestureDetectorRegionState
extends State<RawTouchGestureDetectorRegion> { extends State<RawTouchGestureDetectorRegion> {
Offset _cacheLongPressPosition = Offset(0, 0); Offset _cacheLongPressPosition = Offset(0, 0);
// Timestamp of the last long press event.
int _cacheLongPressPositionTs = 0;
double _mouseScrollIntegral = 0; // mouse scroll speed controller double _mouseScrollIntegral = 0; // mouse scroll speed controller
double _scale = 1; double _scale = 1;
@@ -95,8 +97,9 @@ class _RawTouchGestureDetectorRegionState
} }
if (handleTouch) { if (handleTouch) {
// Desktop or mobile "Touch mode" // Desktop or mobile "Touch mode"
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
inputModel.tapDown(MouseButtons.left); inputModel.tapDown(MouseButtons.left);
}
} }
} }
@@ -105,8 +108,9 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (handleTouch) { if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); if (ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy)) {
inputModel.tapUp(MouseButtons.left); inputModel.tapUp(MouseButtons.left);
}
} }
} }
@@ -134,6 +138,9 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) {
return;
}
inputModel.tap(MouseButtons.left); inputModel.tap(MouseButtons.left);
inputModel.tap(MouseButtons.left); inputModel.tap(MouseButtons.left);
} }
@@ -146,6 +153,7 @@ class _RawTouchGestureDetectorRegionState
if (handleTouch) { if (handleTouch) {
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
_cacheLongPressPosition = d.localPosition; _cacheLongPressPosition = d.localPosition;
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
} }
} }
@@ -203,7 +211,7 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (!handleTouch) { if (!handleTouch) {
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch); ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
} }
@@ -222,6 +230,22 @@ class _RawTouchGestureDetectorRegionState
return; return;
} }
if (handleTouch) { if (handleTouch) {
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
if (isDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords();
}
// Workaround for the issue that the first pan event is sent a long time after the start event.
// If the time interval between the start event and the first pan event is less than 500ms,
// we consider to use the long press position as the start position.
//
// TODO: We should find a better way to send the first pan event as soon as possible.
if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs <
500) {
ffi.cursorModel
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
}
inputModel.sendMouse('down', MouseButtons.left); inputModel.sendMouse('down', MouseButtons.left);
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else { } else {
@@ -241,13 +265,19 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch); if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return;
}
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
} }
onOneFingerPanEnd(DragEndDetails d) { onOneFingerPanEnd(DragEndDetails d) {
if (lastDeviceKind != PointerDeviceKind.touch) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
inputModel.sendMouse('up', MouseButtons.left); inputModel.sendMouse('up', MouseButtons.left);
} }
@@ -275,7 +305,7 @@ class _RawTouchGestureDetectorRegionState
} }
} else { } else {
// mobile // mobile
ffi.canvasModel.updateScale(d.scale / _scale); ffi.canvasModel.updateScale(d.scale / _scale, d.focalPoint);
_scale = d.scale; _scale = d.scale;
ffi.canvasModel.panX(d.focalPointDelta.dx); ffi.canvasModel.panX(d.focalPointDelta.dx);
ffi.canvasModel.panY(d.focalPointDelta.dy); ffi.canvasModel.panY(d.focalPointDelta.dy);

View File

@@ -3,15 +3,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
customImageQualityWidget( customImageQualityWidget(
{required double initQuality, {required double initQuality,
required double initFps, required double initFps,
required Function(double) setQuality, required Function(double)? setQuality,
required Function(double) setFps, required Function(double)? setFps,
required bool showFps, required bool showFps,
required bool showMoreQuality}) { required bool showMoreQuality}) {
if (initQuality < kMinQuality || if (initQuality < kMinQuality ||
@@ -27,16 +26,12 @@ customImageQualityWidget(
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality); final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
final debouncerQuality = Debouncer<double>( final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000), Duration(milliseconds: 1000),
onChanged: (double v) { onChanged: setQuality,
setQuality(v);
},
initialValue: qualityValue.value, initialValue: qualityValue.value,
); );
final debouncerFps = Debouncer<double>( final debouncerFps = Debouncer<double>(
Duration(milliseconds: 1000), Duration(milliseconds: 1000),
onChanged: (double v) { onChanged: setFps,
setFps(v);
},
initialValue: fpsValue.value, initialValue: fpsValue.value,
); );
@@ -62,10 +57,12 @@ customImageQualityWidget(
divisions: moreQualityChecked.value divisions: moreQualityChecked.value
? ((kMaxMoreQuality - kMinQuality) / 10).round() ? ((kMaxMoreQuality - kMinQuality) / 10).round()
: ((kMaxQuality - kMinQuality) / 5).round(), : ((kMaxQuality - kMinQuality) / 5).round(),
onChanged: (double value) async { onChanged: setQuality == null
qualityValue.value = value; ? null
debouncerQuality.value = value; : (double value) async {
}, qualityValue.value = value;
debouncerQuality.value = value;
},
), ),
), ),
Expanded( Expanded(
@@ -124,10 +121,12 @@ customImageQualityWidget(
min: kMinFps, min: kMinFps,
max: kMaxFps, max: kMaxFps,
divisions: ((kMaxFps - kMinFps) / 5).round(), divisions: ((kMaxFps - kMinFps) / 5).round(),
onChanged: (double value) async { onChanged: setFps == null
fpsValue.value = value; ? null
debouncerFps.value = value; : (double value) async {
}, fpsValue.value = value;
debouncerFps.value = value;
},
), ),
), ),
Expanded( Expanded(
@@ -152,21 +151,29 @@ customImageQualitySetting() {
final qualityKey = 'custom_image_quality'; final qualityKey = 'custom_image_quality';
final fpsKey = 'custom-fps'; final fpsKey = 'custom-fps';
var initQuality = final initQuality =
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
kDefaultQuality); kDefaultQuality);
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? final isQuanlityFixed = isOptionFixed(qualityKey);
kDefaultFps); final initFps =
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
kDefaultFps);
final isFpsFixed = isOptionFixed(fpsKey);
return customImageQualityWidget( return customImageQualityWidget(
initQuality: initQuality, initQuality: initQuality,
initFps: initFps, initFps: initFps,
setQuality: (v) { setQuality: isQuanlityFixed
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString()); ? null
}, : (v) {
setFps: (v) { bind.mainSetUserDefaultOption(
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString()); key: qualityKey, value: v.toString());
}, },
setFps: isFpsFixed
? null
: (v) {
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
},
showFps: true, showFps: true,
showMoreQuality: true); showMoreQuality: true);
} }
@@ -208,27 +215,31 @@ List<Widget> ServerConfigImportExportWidgets(
List<(String, String)> otherDefaultSettings() { List<(String, String)> otherDefaultSettings() {
List<(String, String)> v = [ List<(String, String)> v = [
('View Mode', 'view_only'), ('View Mode', kOptionViewOnly),
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar), if ((isDesktop || isWebDesktop))
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'), ('show_monitors_tip', kKeyShowMonitorsToolbar),
('Show remote cursor', 'show_remote_cursor'), if ((isDesktop || isWebDesktop))
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'), ('Collapse toolbar', kOptionCollapseToolbar),
('Show quality monitor', 'show_quality_monitor'), ('Show remote cursor', kOptionShowRemoteCursor),
('Mute', 'disable_audio'), ('Follow remote cursor', kOptionFollowRemoteCursor),
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'), ('Follow remote window focus', kOptionFollowRemoteWindow),
('Disable clipboard', 'disable_clipboard'), if ((isDesktop || isWebDesktop)) ('Zoom cursor', kOptionZoomCursor),
('Lock after session end', 'lock_after_session_end'), ('Show quality monitor', kOptionShowQualityMonitor),
('Privacy mode', 'privacy_mode'), ('Mute', kOptionDisableAudio),
if (isMobile) ('Touch mode', 'touch-mode'), if (isDesktop) ('Enable file copy and paste', kOptionEnableFileCopyPaste),
('True color (4:4:4)', 'i444'), ('Disable clipboard', kOptionDisableClipboard),
('Lock after session end', kOptionLockAfterSessionEnd),
('Privacy mode', kOptionPrivacyMode),
if (isMobile) ('Touch mode', kOptionTouchMode),
('True color (4:4:4)', kOptionI444),
('Reverse mouse wheel', kKeyReverseMouseWheel), ('Reverse mouse wheel', kKeyReverseMouseWheel),
('swap-left-right-mouse', 'swap-left-right-mouse'), ('swap-left-right-mouse', kOptionSwapLeftRightMouse),
if (isDesktop && useTextureRender) if (isDesktop)
( (
'Show displays as individual windows', 'Show displays as individual windows',
kKeyShowDisplaysAsIndividualWindows kKeyShowDisplaysAsIndividualWindows
), ),
if (isDesktop && useTextureRender) if (isDesktop)
( (
'Use all my displays for the remote session', 'Use all my displays for the remote session',
kKeyUseAllMyDisplaysForTheRemoteSession kKeyUseAllMyDisplaysForTheRemoteSession

View File

@@ -8,7 +8,6 @@ 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/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
bool isEditOsPassword = false; bool isEditOsPassword = false;
@@ -116,7 +115,10 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
); );
} }
// paste // paste
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) { if (isMobile &&
pi.platform != kPeerPlatformAndroid &&
perms['keyboard'] != false &&
perms['clipboard'] != false) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Paste')), child: Text(translate('Paste')),
onPressed: () async { onPressed: () async {
@@ -327,7 +329,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
final alternativeCodecs = final alternativeCodecs =
await bind.sessionAlternativeCodecs(sessionId: sessionId); await bind.sessionAlternativeCodecs(sessionId: sessionId);
final groupValue = await bind.sessionGetOption( final groupValue = await bind.sessionGetOption(
sessionId: sessionId, arg: 'codec-preference') ?? sessionId: sessionId, arg: kOptionCodecPreference) ??
''; '';
final List<bool> codecs = []; final List<bool> codecs = [];
try { try {
@@ -349,20 +351,25 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
onChanged(String? value) async { onChanged(String? value) async {
if (value == null) return; if (value == null) return;
await bind.sessionPeerOption( await bind.sessionPeerOption(
sessionId: sessionId, name: 'codec-preference', value: value); sessionId: sessionId, name: kOptionCodecPreference, value: value);
bind.sessionChangePreferCodec(sessionId: sessionId); bind.sessionChangePreferCodec(sessionId: sessionId);
} }
TRadioMenu<String> radio(String label, String value, bool enabled) { TRadioMenu<String> radio(String label, String value, bool enabled) {
return TRadioMenu<String>( return TRadioMenu<String>(
child: Text(translate(label)), child: Text(label),
value: value, value: value,
groupValue: groupValue, groupValue: groupValue,
onChanged: enabled ? onChanged : null); onChanged: enabled ? onChanged : null);
} }
var autoLabel = translate('Auto');
if (groupValue == 'auto' &&
ffi.qualityMonitorModel.data.codecFormat != null) {
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
}
return [ return [
radio('Auto', 'auto', true), radio(autoLabel, 'auto', true),
if (codecs[0]) radio('VP8', 'vp8', codecs[0]), if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
radio('VP9', 'vp9', true), radio('VP9', 'vp9', true),
if (codecs[1]) radio('AV1', 'av1', codecs[1]), if (codecs[1]) radio('AV1', 'av1', codecs[1]),
@@ -371,12 +378,11 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
]; ];
} }
Future<List<TToggleMenu>> toolbarDisplayToggle( Future<List<TToggleMenu>> toolbarCursor(
BuildContext context, String id, FFI ffi) async { BuildContext context, String id, FFI ffi) async {
List<TToggleMenu> v = []; List<TToggleMenu> v = [];
final ffiModel = ffi.ffiModel; final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi; final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
// show remote cursor // show remote cursor
@@ -384,12 +390,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
!ffi.canvasModel.cursorEmbedded && !ffi.canvasModel.cursorEmbedded &&
!pi.isWayland) { !pi.isWayland) {
final state = ShowRemoteCursorState.find(id); final state = ShowRemoteCursorState.find(id);
final lockState = ShowRemoteCursorLockState.find(id);
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'show-remote-cursor'; final option = 'show-remote-cursor';
if (pi.currentDisplay == kAllDisplayValue ||
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
lockState.value = false;
}
v.add(TToggleMenu( v.add(TToggleMenu(
child: Text(translate('Show remote cursor')), child: Text(translate('Show remote cursor')),
value: state.value, value: state.value,
onChanged: enabled onChanged: enabled && !lockState.value
? (value) async { ? (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption( await bind.sessionToggleOption(
@@ -399,6 +410,67 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
} }
: null)); : null));
} }
// follow remote cursor
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland &&
versionCmp(pi.version, "1.2.4") >= 0 &&
pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
final option = 'follow-remote-cursor';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
final showCursorOption = 'show-remote-cursor';
final showCursorState = ShowRemoteCursorState.find(id);
final showCursorLockState = ShowRemoteCursorLockState.find(id);
final showCursorEnabled = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
showCursorLockState.value = value;
if (value && !showCursorEnabled) {
await bind.sessionToggleOption(
sessionId: sessionId, value: showCursorOption);
showCursorState.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
}
v.add(TToggleMenu(
child: Text(translate('Follow remote cursor')),
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
showCursorLockState.value = value;
if (!showCursorEnabled) {
await bind.sessionToggleOption(
sessionId: sessionId, value: showCursorOption);
showCursorState.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
}
}));
}
// follow remote window focus
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland &&
versionCmp(pi.version, "1.2.4") >= 0 &&
pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
final option = 'follow-remote-window';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu(
child: Text(translate('Follow remote window focus')),
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
}));
}
// zoom cursor // zoom cursor
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? ''; final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
if (!isMobile && if (!isMobile &&
@@ -417,6 +489,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
}, },
)); ));
} }
return v;
}
Future<List<TToggleMenu>> toolbarDisplayToggle(
BuildContext context, String id, FFI ffi) async {
List<TToggleMenu> v = [];
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
// show quality monitor // show quality monitor
final option = 'show-quality-monitor'; final option = 'show-quality-monitor';
v.add(TToggleMenu( v.add(TToggleMenu(
@@ -441,20 +524,27 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Mute')))); child: Text(translate('Mute'))));
} }
// file copy and paste // file copy and paste
// If the version is less than 1.2.4, file copy and paste is supported on Windows only.
final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
isWindows &&
pi.platform == kPeerPlatformWindows;
// If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
if (ffiModel.keyboard && if (ffiModel.keyboard &&
perms['file'] != false && perms['file'] != false &&
bind.mainHasFileClipboard() && (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'enable-file-transfer'; final value = bind.sessionGetToggleOptionSync(
final value = sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu( v.add(TToggleMenu(
value: value, value: value,
onChanged: enabled onChanged: enabled
? (value) { ? (value) {
if (value == null) return; if (value == null) return;
bind.sessionToggleOption(sessionId: sessionId, value: option); bind.sessionToggleOption(
sessionId: sessionId, value: kOptionEnableFileCopyPaste);
} }
: null, : null,
child: Text(translate('Enable file copy and paste')))); child: Text(translate('Enable file copy and paste'))));
@@ -477,7 +567,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Disable clipboard')))); child: Text(translate('Disable clipboard'))));
} }
// lock after session end // lock after session end
if (ffiModel.keyboard) { if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
final enabled = !ffiModel.viewOnly; final enabled = !ffiModel.viewOnly;
final option = 'lock-after-session-end'; final option = 'lock-after-session-end';
final value = final value =
@@ -493,8 +583,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Lock after session end')))); child: Text(translate('Lock after session end'))));
} }
if (useTextureRender && if (pi.isSupportMultiDisplay &&
pi.isSupportMultiDisplay &&
PrivacyModeState.find(id).isEmpty && PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 && pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') { bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
@@ -506,13 +595,13 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
bind.sessionSetDisplaysAsIndividualWindows( bind.sessionSetDisplaysAsIndividualWindows(
sessionId: sessionId, value: value ? 'Y' : ''); sessionId: sessionId, value: value ? 'Y' : 'N');
}, },
child: Text(translate('Show displays as individual windows')))); child: Text(translate('Show displays as individual windows'))));
} }
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1; final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) { if (pi.isSupportMultiDisplay && isMultiScreens) {
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession( final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
sessionId: ffi.sessionId) == sessionId: ffi.sessionId) ==
'Y'; 'Y';
@@ -521,7 +610,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
bind.sessionSetUseAllMyDisplaysForTheRemoteSession( bind.sessionSetUseAllMyDisplaysForTheRemoteSession(
sessionId: sessionId, value: value ? 'Y' : ''); sessionId: sessionId, value: value ? 'Y' : 'N');
}, },
child: Text(translate('Use all my displays for the remote session')))); child: Text(translate('Use all my displays for the remote session'))));
} }

View File

@@ -19,7 +19,11 @@ const kKeyTranslateMode = 'translate';
const String kPlatformAdditionsIsWayland = "is_wayland"; const String kPlatformAdditionsIsWayland = "is_wayland";
const String kPlatformAdditionsHeadless = "headless"; const String kPlatformAdditionsHeadless = "headless";
const String kPlatformAdditionsIsInstalled = "is_installed"; const String kPlatformAdditionsIsInstalled = "is_installed";
const String kPlatformAdditionsVirtualDisplays = "virtual_displays"; const String kPlatformAdditionsIddImpl = "idd_impl";
const String kPlatformAdditionsRustDeskVirtualDisplays =
"rustdesk_virtual_displays";
const String kPlatformAdditionsAmyuniVirtualDisplays =
"amyuni_virtual_displays";
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl = const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl"; "supported_privacy_mode_impl";
@@ -58,16 +62,87 @@ const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list"; const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list"; const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
const String kWindowEventSetFullscreen = "set_fullscreen";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventGetCachedSessionData = "get_cached_session_data"; const String kWindowEventGetCachedSessionData = "get_cached_session_data";
const String kWindowEventOpenMonitorSession = "open_monitor_session"; const String kWindowEventOpenMonitorSession = "open_monitor_session";
const String kOptionViewStyle = "view_style";
const String kOptionScrollStyle = "scroll_style";
const String kOptionImageQuality = "image_quality";
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
const String kOptionTextureRender = "use-texture-render";
const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInTabs = "allow-open-in-tabs";
const String kOptionOpenInWindows = "allow-open-in-windows"; const String kOptionOpenInWindows = "allow-open-in-windows";
const String kOptionForceAlwaysRelay = "force-always-relay"; const String kOptionForceAlwaysRelay = "force-always-relay";
const String kOptionViewOnly = "view-only"; const String kOptionViewOnly = "view_only";
const String kOptionEnableLanDiscovery = "enable-lan-discovery";
const String kOptionWhitelist = "whitelist";
const String kOptionEnableAbr = "enable-abr";
const String kOptionEnableRecordSession = "enable-record-session";
const String kOptionDirectServer = "direct-server";
const String kOptionDirectAccessPort = "direct-access-port";
const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
const String kOptionEnableHwcodec = "enable-hwcodec";
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
const String kOptionVideoSaveDirectory = "video-save-directory";
const String kOptionAccessMode = "access-mode";
const String kOptionEnableKeyboard = "enable-keyboard";
// "Settings -> Security -> Permissions"
const String kOptionEnableClipboard = "enable-clipboard";
const String kOptionEnableFileTransfer = "enable-file-transfer";
const String kOptionEnableAudio = "enable-audio";
const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input";
const String kOptionAllowRemoteConfigModification =
"allow-remote-config-modification";
const String kOptionVerificationMethod = "verification-method";
const String kOptionApproveMode = "approve-mode";
const String kOptionCollapseToolbar = "collapse_toolbar";
const String kOptionShowRemoteCursor = "show_remote_cursor";
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
const String kOptionFollowRemoteWindow = "follow_remote_window";
const String kOptionZoomCursor = "zoom-cursor";
const String kOptionShowQualityMonitor = "show_quality_monitor";
const String kOptionDisableAudio = "disable_audio";
const String kOptionEnableFileCopyPaste = "enable-file-copy-paste";
// "Settings -> Display -> Other default options"
const String kOptionDisableClipboard = "disable_clipboard";
const String kOptionLockAfterSessionEnd = "lock_after_session_end";
const String kOptionPrivacyMode = "privacy_mode";
const String kOptionTouchMode = "touch-mode";
const String kOptionI444 = "i444";
const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
const String kOptionCodecPreference = "codec-preference";
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
const String kOptionRemoteMenubarState = "remoteMenubarState";
const String kOptionPeerSorting = "peer-sorting";
const String kOptionPeerTabIndex = "peer-tab-index";
const String kOptionPeerTabOrder = "peer-tab-order";
const String kOptionPeerTabVisible = "peer-tab-visible";
const String kOptionPeerCardUiType = "peer-card-ui-type";
const String kOptionCurrentAbName = "current-ab-name";
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
const String kOptionEnableCheckUpdate = "enable-check-update";
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
const String kOptionStopService = "stop-service";
const String kOptionDirectxCapture = "enable-directx-capture";
const String kOptionToggleViewOnly = "view-only";
const String kOptionDisableFloatingWindow = "disable-floating-window";
const String kOptionKeepScreenOn = "keep-screen-on";
const String kOptionShowMobileAction = "showMobileActions";
const String kUrlActionClose = "close"; const String kUrlActionClose = "close";
@@ -87,10 +162,13 @@ const String kKeyUseAllMyDisplaysForTheRemoteSession =
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar'; const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
const String kKeyReverseMouseWheel = "reverse_mouse_wheel"; const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
const String kMsgboxTextWaitingForImage = 'Connected, waiting for image...';
// the executable name of the portable version // the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
const Color kColorCanvas = Colors.black;
const int kMobileDefaultDisplayWidth = 720; const int kMobileDefaultDisplayWidth = 720;
const int kMobileDefaultDisplayHeight = 1280; const int kMobileDefaultDisplayHeight = 1280;
@@ -121,12 +199,11 @@ double kNewWindowOffset = isWindows
? 30.0 ? 30.0
: 50.0; : 50.0;
EdgeInsets get kDragToResizeAreaPadding => EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
!kUseCompatibleUiMode && isLinux ? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value ? EdgeInsets.zero
? EdgeInsets.zero : EdgeInsets.all(5.0)
: EdgeInsets.all(5.0) : EdgeInsets.zero;
: EdgeInsets.zero;
// https://en.wikipedia.org/wiki/Non-breaking_space // https://en.wikipedia.org/wiki/Non-breaking_space
const int $nbsp = 0x00A0; const int $nbsp = 0x00A0;
@@ -150,9 +227,15 @@ 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;
var kWindowEdgeSize = isWindows ? 1.0 : 5.0; // Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
const kWindowBorderWidth = 1.0; final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
final 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 kFrameClipRRectBorderRadius = 12.0;
const kFrameBoxShadowBlurRadius = 32.0;
const kFrameBoxShadowOffsetFocused = 4.0;
const kFrameBoxShadowOffsetUnfocused = 2.0;
const kInvalidValueStr = 'InvalidValueStr'; const kInvalidValueStr = 'InvalidValueStr';
@@ -198,12 +281,6 @@ const kRemoteImageQualityLow = 'low';
/// [kRemoteImageQualityCustom] Custom image quality. /// [kRemoteImageQualityCustom] Custom image quality.
const kRemoteImageQualityCustom = 'custom'; const kRemoteImageQualityCustom = 'custom';
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
const kRemoteAudioGuestToHost = 'guest-to-host';
/// [kRemoteAudioDualWay] dual-way audio mode(default).
const kRemoteAudioDualWay = 'dual-way';
const kIgnoreDpi = true; const kIgnoreDpi = true;
// ================================ mobile ================================ // ================================ mobile ================================
@@ -222,6 +299,7 @@ const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
const kRequestIgnoreBatteryOptimizations = const kRequestIgnoreBatteryOptimizations =
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW"; const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
const kAndroid13Notification = "android.permission.POST_NOTIFICATIONS";
/// Android channel invoke type key /// Android channel invoke type key
class AndroidChannel { class AndroidChannel {

View File

@@ -262,7 +262,7 @@ class _ConnectionPageState extends State<ConnectionPage>
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 ? kMaximizeEdgeSize : kWindowEdgeSize; stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
} }
@override @override

View File

@@ -69,44 +69,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
); );
} }
Widget buildPresetPasswordWarning() {
return FutureBuilder<bool>(
future: bind.isPresetPassword(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
} else if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
} else if (snapshot.hasData && snapshot.data == true) {
return Container(
color: Colors.yellow,
child: Column(
children: [
Align(
child: Text(
translate("Security Alert"),
style: TextStyle(
color: Colors.red,
fontSize: 20,
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 {
return SizedBox
.shrink(); // Show nothing if the Future completed with false or null
}
},
);
}
Widget buildLeftPane(BuildContext context) { Widget buildLeftPane(BuildContext context) {
final isIncomingOnly = bind.isIncomingOnly(); final isIncomingOnly = bind.isIncomingOnly();
final isOutgoingOnly = bind.isOutgoingOnly(); final isOutgoingOnly = bind.isOutgoingOnly();
@@ -115,22 +77,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
if (bind.isCustomClient()) if (bind.isCustomClient())
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: MouseRegion( child: loadPowered(context),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
launchUrl(Uri.parse('https://rustdesk.com'));
},
child: Opacity(
opacity: 0.5,
child: Text(
translate("powered_by_me"),
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 9, decoration: TextDecoration.underline),
)),
),
).marginOnly(top: 6),
), ),
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
@@ -206,7 +153,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
size: 22, size: 22,
), ),
), ),
onTap: () => DesktopSettingPage.switch2page(0), onTap: () => {
if (DesktopSettingPage.tabKeys.isNotEmpty)
{
DesktopSettingPage.switch2page(
DesktopSettingPage.tabKeys[0])
}
},
onHover: (value) => _editHover.value = value, onHover: (value) => _editHover.value = value,
), ),
), ),
@@ -298,19 +251,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
RxBool hover = false.obs; RxBool hover = false.obs;
return InkWell( return InkWell(
onTap: DesktopTabPage.onAddSetting, onTap: DesktopTabPage.onAddSetting,
child: Obx( child: Tooltip(
() => CircleAvatar( message: translate('Settings'),
radius: 15, child: Obx(
backgroundColor: hover.value () => CircleAvatar(
? Theme.of(context).scaffoldBackgroundColor radius: 15,
: Theme.of(context).colorScheme.background, backgroundColor: hover.value
child: Tooltip( ? Theme.of(context).scaffoldBackgroundColor
message: translate('Settings'), : Theme.of(context).colorScheme.background,
child: Icon( child: Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
size: 20, size: 20,
color: hover.value ? textColor : textColor?.withOpacity(0.5), color: hover.value ? textColor : textColor?.withOpacity(0.5),
)), ),
),
), ),
), ),
onHover: (value) => hover.value = value, onHover: (value) => hover.value = value,
@@ -371,33 +325,36 @@ class _DesktopHomePageState extends State<DesktopHomePage>
), ),
AnimatedRotationWidget( AnimatedRotationWidget(
onPressed: () => bind.mainUpdateTemporaryPassword(), onPressed: () => bind.mainUpdateTemporaryPassword(),
child: Obx(() => RotatedBox( child: Tooltip(
quarterTurns: 2, message: translate('Refresh Password'),
child: Tooltip( child: Obx(() => RotatedBox(
message: translate('Refresh Password'), quarterTurns: 2,
child: Icon( child: Icon(
Icons.refresh, Icons.refresh,
color: refreshHover.value color: refreshHover.value
? textColor ? textColor
: Color(0xFFDDDDDD), : Color(0xFFDDDDDD),
size: 22, size: 22,
)))), ))),
),
onHover: (value) => refreshHover.value = value, onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4), ).marginOnly(right: 8, top: 4),
if (!bind.isDisableSettings()) if (!bind.isDisableSettings())
InkWell( InkWell(
child: Obx( child: Tooltip(
() => Tooltip( message: translate('Change Password'),
message: translate('Change Password'), child: Obx(
child: Icon( () => Icon(
Icons.edit, Icons.edit,
color: editHover.value color: editHover.value
? textColor ? textColor
: Color(0xFFDDDDDD), : Color(0xFFDDDDDD),
size: 22, size: 22,
)).marginOnly(right: 8, top: 4), ).marginOnly(right: 8, top: 4),
),
), ),
onTap: () => DesktopSettingPage.switch2page(0), onTap: () => DesktopSettingPage.switch2page(
SettingsTabKey.safety),
onHover: (value) => editHover.value = value, onHover: (value) => editHover.value = value,
), ),
], ],
@@ -701,10 +658,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Timer(const Duration(seconds: 1), () async { if (!bind.isCustomClient()) {
updateUrl = await bind.mainGetSoftwareUpdateUrl(); Timer(const Duration(seconds: 1), () async {
if (updateUrl.isNotEmpty) setState(() {}); updateUrl = await bind.mainGetSoftwareUpdateUrl();
}); if (updateUrl.isNotEmpty) setState(() {});
});
}
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async { _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID(); await gFFI.serverModel.fetchID();
final error = await bind.mainGetError(); final error = await bind.mainGetError();
@@ -712,7 +671,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
systemError = error; systemError = error;
setState(() {}); setState(() {});
} }
final v = await bind.mainGetOption(key: "stop-service") == "Y"; final v = await mainGetBoolOption(kOptionStopService);
if (v != svcStopped.value) { if (v != svcStopped.value) {
svcStopped.value = v; svcStopped.value = v;
setState(() {}); setState(() {});
@@ -824,6 +783,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final screenRect = parseParamScreenRect(args); final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession( await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount, screenRect); windowId, peerId, display, displayCount, screenRect);
} else if (call.method == kWindowEventRemoteWindowCoords) {
final windowId = int.tryParse(call.arguments);
if (windowId != null) {
return jsonEncode(
await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
}
} }
}); });
_uniLinksSubscription = listenUniLinks(); _uniLinksSubscription = listenUniLinks();

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,8 @@ class DesktopTabPage extends StatefulWidget {
@override @override
State<DesktopTabPage> createState() => _DesktopTabPageState(); State<DesktopTabPage> createState() => _DesktopTabPageState();
static void onAddSetting({int initialPage = 0}) { static void onAddSetting(
{SettingsTabKey initialPage = SettingsTabKey.general}) {
try { try {
DesktopTabController tabController = Get.find(); DesktopTabController tabController = Get.find();
tabController.add(TabInfo( tabController.add(TabInfo(
@@ -27,7 +28,7 @@ class DesktopTabPage extends StatefulWidget {
unselectedIcon: Icons.build_outlined, unselectedIcon: Icons.build_outlined,
page: DesktopSettingPage( page: DesktopSettingPage(
key: const ValueKey(kTabLabelSettingPage), key: const ValueKey(kTabLabelSettingPage),
initialPage: initialPage, initialTabkey: initialPage,
))); )));
} catch (e) { } catch (e) {
debugPrintStack(label: '$e'); debugPrintStack(label: '$e');
@@ -35,12 +36,24 @@ class DesktopTabPage extends StatefulWidget {
} }
} }
class _DesktopTabPageState extends State<DesktopTabPage> { class _DesktopTabPageState extends State<DesktopTabPage>
with WidgetsBindingObserver {
final tabController = DesktopTabController(tabType: DesktopTabType.main); final tabController = DesktopTabController(tabType: DesktopTabType.main);
final RxBool _block = false.obs;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
shouldBeBlocked(_block, canBeBlocked);
} else if (state == AppLifecycleState.inactive) {}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
Get.put<DesktopTabController>(tabController); Get.put<DesktopTabController>(tabController);
RemoteCountState.init(); RemoteCountState.init();
tabController.add(TabInfo( tabController.add(TabInfo(
@@ -56,10 +69,10 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
tabController.onSelected = (key) { tabController.onSelected = (key) {
if (key == kTabLabelHomePage) { if (key == kTabLabelHomePage) {
windowManager.setSize(getIncomingOnlyHomeSize()); windowManager.setSize(getIncomingOnlyHomeSize());
windowManager.setResizable(false); setResizable(false);
} else { } else {
windowManager.setSize(getIncomingOnlySettingsSize()); windowManager.setSize(getIncomingOnlySettingsSize());
windowManager.setResizable(true); setResizable(true);
} }
}; };
} }
@@ -67,8 +80,10 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
@override @override
void dispose() { void dispose() {
super.dispose(); WidgetsBinding.instance.removeObserver(this);
Get.delete<DesktopTabController>(); Get.delete<DesktopTabController>();
super.dispose();
} }
@override @override
@@ -88,12 +103,17 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
), ),
), ),
))); )));
widget() => MouseRegion(
onEnter: (_) async {
await shouldBeBlocked(_block, canBeBlocked);
},
child: FocusScope(child: tabWidget, canRequestFocus: !_block.value));
return isMacOS || kUseCompatibleUiMode return isMacOS || kUseCompatibleUiMode
? tabWidget ? Obx(() => widget())
: Obx( : Obx(
() => DragToResizeArea( () => DragToResizeArea(
resizeEdgeSize: stateGlobal.resizeEdgeSize.value, resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
child: tabWidget, child: widget(),
), ),
); );
} }

View File

@@ -91,18 +91,21 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tabWidget = Container( final child = Scaffold(
decoration: BoxDecoration( backgroundColor: Theme.of(context).cardColor,
border: Border.all(color: MyTheme.color(context).border!)), body: DesktopTab(
child: Scaffold( controller: tabController,
backgroundColor: Theme.of(context).cardColor, onWindowCloseButton: handleWindowCloseButton,
body: DesktopTab( tail: const AddButton(),
controller: tabController, labelGetter: DesktopTab.tablabelGetter,
onWindowCloseButton: handleWindowCloseButton, ));
tail: const AddButton().paddingOnly(left: 10), final tabWidget = isLinux
labelGetter: DesktopTab.tablabelGetter, ? buildVirtualWindowFrame(context, child)
)), : Container(
); decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
);
return isMacOS || kUseCompatibleUiMode return isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: SubWindowDragToResizeArea( : SubWindowDragToResizeArea(
@@ -128,9 +131,9 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
tabController.clear(); tabController.clear();
return true; return true;
} else { } else {
final opt = "enable-confirm-closing-tabs";
final bool res; final bool res;
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) { if (!option2bool(kOptionEnableConfirmClosingTabs,
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
res = true; res = true;
} else { } else {
res = await closeConfirmDialog(); res = await closeConfirmDialog();

View File

@@ -141,8 +141,9 @@ class _PortForwardPageState extends State<PortForwardPage>
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
return Theme( return Theme(
data: Theme.of(context) data: Theme.of(context).copyWith(
.copyWith(backgroundColor: Theme.of(context).colorScheme.background), colorScheme: Theme.of(context).colorScheme,
),
child: Obx(() => ListView.builder( child: Obx(() => ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemCount: pfs.length + 2, itemCount: pfs.length + 2,
@@ -289,7 +290,7 @@ class _PortForwardPageState extends State<PortForwardPage>
).marginOnly(left: _kTextLeftMargin)); ).marginOnly(left: _kTextLeftMargin));
return Theme( return Theme(
data: Theme.of(context) data: Theme.of(context)
.copyWith(backgroundColor: Theme.of(context).colorScheme.background), .copyWith(colorScheme: Theme.of(context).colorScheme),
child: ListView.builder( child: ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemCount: 2, itemCount: 2,

View File

@@ -97,21 +97,30 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tabWidget = Container( final child = Scaffold(
decoration: BoxDecoration( backgroundColor: Theme.of(context).colorScheme.background,
border: Border.all(color: MyTheme.color(context).border!)), body: DesktopTab(
child: Scaffold( controller: tabController,
backgroundColor: Theme.of(context).colorScheme.background, onWindowCloseButton: () async {
body: DesktopTab( tabController.clear();
controller: tabController, return true;
onWindowCloseButton: () async { },
tabController.clear(); tail: AddButton(),
return true; labelGetter: DesktopTab.tablabelGetter,
}, ),
tail: AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.tablabelGetter,
)),
); );
final tabWidget = isLinux
? buildVirtualWindowFrame(
context,
Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: child),
)
: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
);
return isMacOS || kUseCompatibleUiMode return isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: Obx( : Obx(

View File

@@ -7,6 +7,7 @@ import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
import 'package:flutter_hbb/models/state_model.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../common/widgets/overlay.dart'; import '../../common/widgets/overlay.dart';
@@ -15,7 +16,6 @@ import '../../common.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
import '../../common/widgets/toolbar.dart'; import '../../common/widgets/toolbar.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/desktop_render_texture.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../../common/shared_state.dart'; import '../../common/shared_state.dart';
import '../../utils/image.dart'; import '../../utils/image.dart';
@@ -85,6 +85,9 @@ class _RemotePageState extends State<RemotePage>
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
// to identify the toolbar instance and its callback function.
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
Function(bool)? _onEnterOrLeaveImage4Toolbar; Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi; late FFI _ffi;
@@ -93,7 +96,7 @@ class _RemotePageState extends State<RemotePage>
void _initStates(String id) { void _initStates(String id) {
initSharedStates(id); initSharedStates(id);
_zoomCursor = PeerBoolOption.find(id, 'zoom-cursor'); _zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
_showRemoteCursor = ShowRemoteCursorState.find(id); _showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id);
_remoteCursorMoved = RemoteCursorMovedState.find(id); _remoteCursorMoved = RemoteCursorMovedState.find(id);
@@ -131,11 +134,12 @@ class _RemotePageState extends State<RemotePage>
_ffi.ffiModel.updateEventListener(sessionId, widget.id); _ffi.ffiModel.updateEventListener(sessionId, widget.id);
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
_ffi.dialogManager.loadMobileActionsOverlayVisible();
// Session option should be set after models.dart/FFI.start // Session option should be set after models.dart/FFI.start
_showRemoteCursor.value = bind.sessionGetToggleOptionSync( _showRemoteCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'show-remote-cursor'); sessionId: sessionId, arg: 'show-remote-cursor');
_zoomCursor.value = bind.sessionGetToggleOptionSync( _zoomCursor.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: 'zoom-cursor'); sessionId: sessionId, arg: kOptionZoomCursor);
DesktopMultiWindow.addListener(this); DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) { // if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback( // customCursorController.registerNeedUpdateCursorCallback(
@@ -165,6 +169,7 @@ class _RemotePageState extends State<RemotePage>
// and let OS to handle events instead. // and let OS to handle events instead.
_rawKeyFocusNode.unfocus(); _rawKeyFocusNode.unfocus();
} }
stateGlobal.isFocused.value = false;
} }
@override @override
@@ -174,6 +179,7 @@ class _RemotePageState extends State<RemotePage>
if (isWindows) { if (isWindows) {
_isWindowBlur = false; _isWindowBlur = false;
} }
stateGlobal.isFocused.value = true;
} }
@override @override
@@ -206,6 +212,22 @@ class _RemotePageState extends State<RemotePage>
} }
} }
@override
void onWindowEnterFullScreen() {
super.onWindowEnterFullScreen();
if (isMacOS) {
stateGlobal.setFullscreen(true);
}
}
@override
void onWindowLeaveFullScreen() {
super.onWindowLeaveFullScreen();
if (isMacOS) {
stateGlobal.setFullscreen(false);
}
}
@override @override
Future<void> dispose() async { Future<void> dispose() async {
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true; final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
@@ -218,6 +240,8 @@ class _RemotePageState extends State<RemotePage>
_ffi.inputModel.enterOrLeave(false); _ffi.inputModel.enterOrLeave(false);
DesktopMultiWindow.removeListener(this); DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay(); _ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
_ffi.recordingModel.onClose(); _ffi.recordingModel.onClose();
_rawKeyFocusNode.dispose(); _rawKeyFocusNode.dispose();
await _ffi.close(closeSession: closeSession); await _ffi.close(closeSession: closeSession);
@@ -248,9 +272,18 @@ class _RemotePageState extends State<RemotePage>
id: widget.id, id: widget.id,
ffi: _ffi, ffi: _ffi,
state: widget.toolbarState, state: widget.toolbarState,
onEnterOrLeaveImageSetter: (func) => onEnterOrLeaveImageSetter: (id, func) {
_onEnterOrLeaveImage4Toolbar = func, _instanceIdOnEnterOrLeaveImage4Toolbar = id;
onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null, _onEnterOrLeaveImage4Toolbar = func;
},
onEnterOrLeaveImageCleaner: (id) {
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
_onEnterOrLeaveImage4Toolbar = null;
}
},
setRemoteState: setState, setRemoteState: setState,
); );
@@ -258,7 +291,7 @@ class _RemotePageState extends State<RemotePage>
return Stack( return Stack(
children: [ children: [
Container( Container(
color: Colors.black, color: kColorCanvas,
child: RawKeyFocusScope( child: RawKeyFocusScope(
focusNode: _rawKeyFocusNode, focusNode: _rawKeyFocusNode,
onFocusChange: (bool imageFocused) { onFocusChange: (bool imageFocused) {
@@ -287,8 +320,21 @@ class _RemotePageState extends State<RemotePage>
_ffi.ffiModel.waitForFirstImage.isTrue _ffi.ffiModel.waitForFirstImage.isTrue
? emptyOverlay() ? emptyOverlay()
: () { : () {
_ffi.ffiModel.tryShowAndroidActionsOverlay(); if (!_ffi.ffiModel.isPeerAndroid) {
return Offstage(); return Offstage();
} else {
return Obx(() => Offstage(
offstage: _ffi.dialogManager
.mobileActionsOverlayVisible.isFalse,
child: Overlay(initialEntries: [
makeMobileActionsOverlayEntry(
() => _ffi.dialogManager
.setMobileActionsOverlayVisible(false),
ffi: _ffi,
)
]),
));
}
}(), }(),
// Use Overlay to enable rebuild every time on menu button click. // Use Overlay to enable rebuild every time on menu button click.
_ffi.ffiModel.pi.isSet.isTrue _ffi.ffiModel.pi.isSet.isTrue
@@ -435,14 +481,14 @@ class _RemotePageState extends State<RemotePage>
}, onExit: (evt) { }, onExit: (evt) {
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
}, child: LayoutBuilder(builder: (context, constraints) { }, child: LayoutBuilder(builder: (context, constraints) {
Future.delayed(Duration.zero, () { final c = Provider.of<CanvasModel>(context, listen: false);
Provider.of<CanvasModel>(context, listen: false).updateViewStyle(); Future.delayed(Duration.zero, () => c.updateViewStyle());
});
final peerDisplay = CurrentDisplayState.find(widget.id); final peerDisplay = CurrentDisplayState.find(widget.id);
return Obx( return Obx(
() => _ffi.ffiModel.pi.isSet.isFalse () => _ffi.ffiModel.pi.isSet.isFalse
? Container(color: Colors.transparent) ? Container(color: Colors.transparent)
: Obx(() { : Obx(() {
widget.toolbarState.initShow(sessionId);
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value); _ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
return ImagePaint( return ImagePaint(
id: widget.id, id: widget.id,
@@ -572,15 +618,15 @@ class _ImagePaintState extends State<ImagePaint> {
onHover: (evt) {}, onHover: (evt) {},
child: child); child: child);
}); });
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final paintWidth = c.getDisplayWidth() * s; final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s; final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight); final paintSize = Size(paintWidth, paintHeight);
final paintWidget = useTextureRender final paintWidget =
? _BuildPaintTextureRender( m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
c, s, Offset.zero, paintSize, isViewOriginal()) ? _BuildPaintTextureRender(
: _buildScrollbarNonTextureRender(m, paintSize, s); c, s, Offset.zero, paintSize, isViewOriginal())
: _buildScrollbarNonTextureRender(m, paintSize, s);
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
c.updateScrollPercent(); c.updateScrollPercent();
@@ -598,17 +644,18 @@ class _ImagePaintState extends State<ImagePaint> {
)); ));
} else { } else {
if (c.size.width > 0 && c.size.height > 0) { if (c.size.width > 0 && c.size.height > 0) {
final paintWidget = useTextureRender final paintWidget =
? _BuildPaintTextureRender( m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
c, ? _BuildPaintTextureRender(
s, c,
Offset( s,
isLinux ? c.x.toInt().toDouble() : c.x, Offset(
isLinux ? c.y.toInt().toDouble() : c.y, isLinux ? c.x.toInt().toDouble() : c.x,
), isLinux ? c.y.toInt().toDouble() : c.y,
c.size, ),
isViewOriginal()) c.size,
: _buildScrollAuthNonTextureRender(m, c, s); isViewOriginal())
: _buildScrollAutoNonTextureRender(m, c, s);
return mouseRegion(child: _buildListener(paintWidget)); return mouseRegion(child: _buildListener(paintWidget));
} else { } else {
return Container(); return Container();
@@ -624,7 +671,7 @@ class _ImagePaintState extends State<ImagePaint> {
); );
} }
Widget _buildScrollAuthNonTextureRender( Widget _buildScrollAutoNonTextureRender(
ImageModel m, CanvasModel c, double s) { ImageModel m, CanvasModel c, double s) {
return CustomPaint( return CustomPaint(
size: Size(c.size.width, c.size.height), size: Size(c.size.width, c.size.height),

View File

@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
@@ -45,7 +46,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined; static const IconData unselectedIcon = Icons.desktop_windows_outlined;
late ToolbarState _toolbarState;
String? peerId; String? peerId;
bool _isScreenRectSet = false; bool _isScreenRectSet = false;
int? _display; int? _display;
@@ -53,7 +53,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
var connectionMap = RxList<Widget>.empty(growable: true); var connectionMap = RxList<Widget>.empty(growable: true);
_ConnectionTabPageState(Map<String, dynamic> params) { _ConnectionTabPageState(Map<String, dynamic> params) {
_toolbarState = ToolbarState();
RemoteCountState.init(); RemoteCountState.init();
peerId = params['id']; peerId = params['id'];
final sessionId = params['session_id']; final sessionId = params['session_id'];
@@ -90,7 +89,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
display: display, display: display,
displays: displays?.cast<int>(), displays: displays?.cast<int>(),
password: params['password'], password: params['password'],
toolbarState: _toolbarState, toolbarState: ToolbarState(),
tabController: tabController, tabController: tabController,
switchUuid: params['switch_uuid'], switchUuid: params['switch_uuid'],
forceRelay: params['forceRelay'], forceRelay: params['forceRelay'],
@@ -107,107 +106,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
tabController.onRemoved = (_, id) => onRemoveId(id); tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
windowOnTop(windowId());
tryMoveToScreenAndSetFullscreen(screenRect);
if (tabController.length == 0) {
// Show the hidden window.
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
stateGlobal.setFullscreen(true);
}
// Reset the state
stateGlobal.closeOnFullscreen = null;
}
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: RemotePage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: _toolbarState,
tabController: tabController,
switchUuid: switchUuid,
forceRelay: args['forceRelay'],
isSharedPassword: args['isSharedPassword'],
),
));
} else if (call.method == kWindowDisableGrabKeyboard) {
// ???
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
final jumpOk = tabController.jumpToByKey(call.arguments);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventActiveDisplaySession) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final display = args['display'];
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs
.map((e) => e.key)
.toList()
.join(',');
} else if (call.method == kWindowEventGetSessionIdList) {
return tabController.state.value.tabs
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final args = jsonDecode(call.arguments);
final id = args['id'];
final close = args['close'];
try {
final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (close && returnValue != null) {
closeSessionOnDispose[id] = false;
tabController.closeBy(id);
}
}
_update_remote_count();
return returnValue;
});
if (!_isScreenRectSet) { if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
restoreWindowPosition( restoreWindowPosition(
@@ -225,108 +124,107 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
_toolbarState.save();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tabWidget = Obx( final child = Scaffold(
() => Container( backgroundColor: Theme.of(context).colorScheme.background,
decoration: BoxDecoration( body: DesktopTab(
border: Border.all( controller: tabController,
color: MyTheme.color(context).border!, onWindowCloseButton: handleWindowCloseButton,
width: stateGlobal.windowBorderWidth.value), tail: const AddButton(),
), pageViewBuilder: (pageView) => pageView,
child: Scaffold( labelGetter: DesktopTab.tablabelGetter,
backgroundColor: Theme.of(context).colorScheme.background, tabBuilder: (key, icon, label, themeConf) => Obx(() {
body: DesktopTab( final connectionType = ConnectionTypeState.find(key);
controller: tabController, if (!connectionType.isValid()) {
onWindowCloseButton: handleWindowCloseButton, return Row(
tail: const AddButton().paddingOnly(left: 10), mainAxisAlignment: MainAxisAlignment.center,
pageViewBuilder: (pageView) => pageView, children: [
labelGetter: DesktopTab.tablabelGetter, icon,
tabBuilder: (key, icon, label, themeConf) => Obx(() { label,
final connectionType = ConnectionTypeState.find(key); ],
if (!connectionType.isValid()) { );
return Row( } else {
mainAxisAlignment: MainAxisAlignment.center, bool secure =
children: [ connectionType.secure.value == ConnectionType.strSecure;
icon, bool direct =
label, connectionType.direct.value == ConnectionType.strDirect;
], String msgConn;
); if (secure && direct) {
} else { msgConn = translate("Direct and encrypted connection");
bool secure = } else if (secure && !direct) {
connectionType.secure.value == ConnectionType.strSecure; msgConn = translate("Relayed and encrypted connection");
bool direct = } else if (!secure && direct) {
connectionType.direct.value == ConnectionType.strDirect; msgConn = translate("Direct and unencrypted connection");
String msgConn; } else {
if (secure && direct) { msgConn = translate("Relayed and unencrypted connection");
msgConn = translate("Direct and encrypted connection"); }
} else if (secure && !direct) { var msgFingerprint = '${translate('Fingerprint')}:\n';
msgConn = translate("Relayed and encrypted connection"); var fingerprint = FingerprintState.find(key).value;
} else if (!secure && direct) { if (fingerprint.isEmpty) {
msgConn = translate("Direct and unencrypted connection"); fingerprint = 'N/A';
} else { }
msgConn = translate("Relayed and unencrypted connection"); if (fingerprint.length > 5 * 8) {
} var first = fingerprint.substring(0, 39);
var msgFingerprint = '${translate('Fingerprint')}:\n'; var second = fingerprint.substring(40);
var fingerprint = FingerprintState.find(key).value; msgFingerprint += '$first\n$second';
if (fingerprint.isEmpty) { } else {
fingerprint = 'N/A'; msgFingerprint += fingerprint;
} }
if (fingerprint.length > 5 * 8) {
var first = fingerprint.substring(0, 39);
var second = fingerprint.substring(40);
msgFingerprint += '$first\n$second';
} else {
msgFingerprint += fingerprint;
}
final tab = Row( final tab = Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
icon, icon,
Tooltip( Tooltip(
message: '$msgConn\n$msgFingerprint', message: '$msgConn\n$msgFingerprint',
child: SvgPicture.asset( child: SvgPicture.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
width: themeConf.iconSize, width: themeConf.iconSize,
height: themeConf.iconSize, height: themeConf.iconSize,
).paddingOnly(right: 5), ).paddingOnly(right: 5),
), ),
label, label,
unreadMessageCountBuilder(UnreadChatCountState.find(key)) unreadMessageCountBuilder(UnreadChatCountState.find(key))
.marginOnly(left: 4), .marginOnly(left: 4),
], ],
); );
return Listener( return Listener(
onPointerDown: (e) { onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) { if (e.kind != ui.PointerDeviceKind.mouse) {
return; return;
} }
final remotePage = tabController.state.value.tabs final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key) .firstWhere((tab) => tab.key == key)
.page as RemotePage; .page as RemotePage;
if (remotePage.ffi.ffiModel.pi.isSet.isTrue && if (remotePage.ffi.ffiModel.pi.isSet.isTrue && e.buttons == 2) {
e.buttons == 2) { showRightMenu(
showRightMenu( (CancelFunc cancelFunc) {
(CancelFunc cancelFunc) { return _tabMenuBuilder(key, cancelFunc);
return _tabMenuBuilder(key, cancelFunc); },
}, target: e.position,
target: e.position, );
); }
} },
}, child: tab,
child: tab, );
); }
} }),
}),
),
),
), ),
); );
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: Obx(() => Container(
decoration: BoxDecoration(
border: Border.all(
color: MyTheme.color(context).border!,
width: stateGlobal.windowBorderWidth.value),
),
child: child,
));
return isMacOS || kUseCompatibleUiMode return isMacOS || kUseCompatibleUiMode
? tabWidget ? tabWidget
: Obx(() => SubWindowDragToResizeArea( : Obx(() => SubWindowDragToResizeArea(
@@ -350,15 +248,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final pi = ffi.ffiModel.pi; final pi = ffi.ffiModel.pi;
final perms = ffi.ffiModel.permissions; final perms = ffi.ffiModel.permissions;
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
final toolbarState = remotePage.toolbarState;
menu.addAll([ menu.addAll([
MenuEntryButton<String>( MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text( childBuilder: (TextStyle? style) => Obx(() => Text(
translate( translate(
_toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
style: style, style: style,
)), )),
proc: () { proc: () {
_toolbarState.switchShow(); toolbarState.switchShow(sessionId);
cancelFunc(); cancelFunc();
}, },
padding: padding, padding: padding,
@@ -449,7 +348,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
void onRemoveId(String id) async { void onRemoveId(String id) async {
if (tabController.state.value.tabs.isEmpty) { if (tabController.state.value.tabs.isEmpty) {
stateGlobal.setFullscreen(false, procWnd: false);
// Keep calling until the window status is hidden. // Keep calling until the window status is hidden.
// //
// Workaround for Windows: // Workaround for Windows:
@@ -483,9 +381,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
tabController.clear(); tabController.clear();
return true; return true;
} else { } else {
final opt = "enable-confirm-closing-tabs";
final bool res; final bool res;
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) { if (!option2bool(kOptionEnableConfirmClosingTabs,
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
res = true; res = true;
} else { } else {
res = await closeConfirmDialog(); res = await closeConfirmDialog();
@@ -499,4 +397,130 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
_update_remote_count() => _update_remote_count() =>
RemoteCountState.find().value = tabController.length; RemoteCountState.find().value = tabController.length;
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
Future.delayed(Duration.zero, () async {
if (stateGlobal.fullscreen.isTrue) {
await WindowController.fromWindowId(windowId()).setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false);
}
await setNewConnectWindowFrame(windowId(), id!, display, screenRect);
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
await windowOnTop(windowId());
});
});
ConnectionTypeState.init(id);
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: RemotePage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: ToolbarState(),
tabController: tabController,
switchUuid: switchUuid,
forceRelay: args['forceRelay'],
isSharedPassword: args['isSharedPassword'],
),
));
} else if (call.method == kWindowDisableGrabKeyboard) {
// ???
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
final jumpOk = tabController.jumpToByKey(call.arguments);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventActiveDisplaySession) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final display = args['display'];
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs
.map((e) => e.key)
.toList()
.join(',');
} else if (call.method == kWindowEventGetSessionIdList) {
return tabController.state.value.tabs
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final args = jsonDecode(call.arguments);
final id = args['id'];
final close = args['close'];
try {
final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (close && returnValue != null) {
closeSessionOnDispose[id] = false;
tabController.closeBy(id);
}
} else if (call.method == kWindowEventRemoteWindowCoords) {
final remotePage =
tabController.state.value.selectedTabInfo.page as RemotePage;
final ffi = remotePage.ffi;
final displayRect = ffi.ffiModel.displaysRect();
if (displayRect != null) {
final wc = WindowController.fromWindowId(windowId());
Rect? frame;
try {
frame = await wc.getFrame();
} catch (e) {
debugPrint(
"Failed to get frame of window $windowId, it may be hidden");
}
if (frame != null) {
ffi.cursorModel.moveLocal(0, 0);
final coords = RemoteWindowCoords(
frame,
CanvasCoords.fromCanvasModel(ffi.canvasModel),
CursorCoords.fromCursorModel(ffi.cursorModel),
displayRect);
returnValue = jsonEncode(coords.toJson());
}
}
} else if (call.method == kWindowEventSetFullscreen) {
stateGlobal.setFullscreen(call.arguments == 'true');
}
_update_remote_count();
return returnValue;
}
} }

View File

@@ -4,6 +4,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/audio_input.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
@@ -76,14 +77,20 @@ class _DesktopServerPageState extends State<DesktopServerPage>
ChangeNotifierProvider.value(value: gFFI.chatModel), ChangeNotifierProvider.value(value: gFFI.chatModel),
], ],
child: Consumer<ServerModel>( child: Consumer<ServerModel>(
builder: (context, serverModel, child) => Container( builder: (context, serverModel, child) {
decoration: BoxDecoration( final body = Scaffold(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: ConnectionManager(), body: ConnectionManager(),
), );
), return isLinux
? buildVirtualWindowFrame(context, body)
: Container(
decoration: BoxDecoration(
border:
Border.all(color: MyTheme.color(context).border!)),
child: body,
);
},
), ),
); );
} }
@@ -157,7 +164,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
controller: serverModel.tabController, controller: serverModel.tabController,
selectedBorderColor: MyTheme.accent, selectedBorderColor: MyTheme.accent,
maxLabelWidth: 100, maxLabelWidth: 100,
tail: buildScrollJumper(), tail: null, //buildScrollJumper(),
selectedTabBackgroundColor: selectedTabBackgroundColor:
Theme.of(context).hintColor.withOpacity(0), Theme.of(context).hintColor.withOpacity(0),
tabBuilder: (key, icon, label, themeConf) { tabBuilder: (key, icon, label, themeConf) {
@@ -282,9 +289,9 @@ class ConnectionManagerState extends State<ConnectionManager> {
windowManager.close(); windowManager.close();
return true; return true;
} else { } else {
final opt = "enable-confirm-closing-tabs";
final bool res; final bool res;
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) { if (!option2bool(kOptionEnableConfirmClosingTabs,
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
res = true; res = true;
} else { } else {
res = await closeConfirmDialog(); res = await closeConfirmDialog();
@@ -701,17 +708,86 @@ class _CmControlPanel extends StatelessWidget {
children: [ children: [
Offstage( Offstage(
offstage: !client.inVoiceCall, offstage: !client.inVoiceCall,
child: buildButton( child: Row(
context, children: [
color: Colors.red, Expanded(
onClick: () => closeVoiceCall(), child: buildButton(context,
icon: Icon( color: MyTheme.accent,
Icons.call_end_rounded, onClick: null, onTapDown: (details) async {
color: Colors.white, final devicesInfo = await AudioInput.getDevicesInfo();
size: 14, List<String> devices = devicesInfo['devices'] as List<String>;
), if (devices.isEmpty) {
text: "Stop voice call", msgBox(
textColor: Colors.white, gFFI.sessionId,
'custom-nocancel-info',
'Prompt',
'no_audio_input_device_tip',
'',
gFFI.dialogManager,
);
return;
}
String currentDevice = devicesInfo['current'] as String;
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
final position = RelativeRect.fromLTRB(x, y, x, y);
showMenu(
context: context,
position: position,
items: devices
.map((d) => PopupMenuItem<String>(
value: d,
height: 18,
padding: EdgeInsets.zero,
onTap: () => AudioInput.setDevice(d),
child: IgnorePointer(
child: RadioMenuButton(
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) AudioInput.setDevice(v);
},
child: Container(
child: Text(
d,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
constraints: BoxConstraints(
maxWidth:
kConnectionManagerWindowSizeClosedChat
.width -
80),
),
)),
))
.toList(),
);
},
icon: Icon(
Icons.call_rounded,
color: Colors.white,
size: 14,
),
text: "Audio input",
textColor: Colors.white),
),
Expanded(
child: buildButton(
context,
color: Colors.red,
onClick: () => closeVoiceCall(),
icon: Icon(
Icons.call_end_rounded,
color: Colors.white,
size: 14,
),
text: "Stop voice call",
textColor: Colors.white,
),
)
],
), ),
), ),
Offstage( Offstage(
@@ -872,12 +948,14 @@ class _CmControlPanel extends StatelessWidget {
Widget buildButton(BuildContext context, Widget buildButton(BuildContext context,
{required Color? color, {required Color? color,
required Function() onClick, GestureTapCallback? onClick,
Icon? icon, Widget? icon,
BoxBorder? border, BoxBorder? border,
required String text, required String text,
required Color? textColor, required Color? textColor,
String? tooltip}) { String? tooltip,
GestureTapDownCallback? onTapDown}) {
assert(!(onClick == null && onTapDown == null));
Widget textWidget; Widget textWidget;
if (icon != null) { if (icon != null) {
textWidget = Text( textWidget = Text(
@@ -901,7 +979,16 @@ class _CmControlPanel extends StatelessWidget {
color: color, borderRadius: borderRadius, border: border), color: color, borderRadius: borderRadius, border: border),
child: InkWell( child: InkWell(
borderRadius: borderRadius, borderRadius: borderRadius,
onTap: () => checkClickTime(client.id, onClick), onTap: () {
if (onClick == null) return;
checkClickTime(client.id, onClick);
},
onTapDown: (details) {
if (onTapDown == null) return;
checkClickTime(client.id, () {
onTapDown.call(details);
});
},
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [

View File

@@ -20,6 +20,7 @@ class DesktopFileTransferScreen extends StatelessWidget {
ChangeNotifierProvider.value(value: gFFI.canvasModel), ChangeNotifierProvider.value(value: gFFI.canvasModel),
], ],
child: Scaffold( child: Scaffold(
backgroundColor: isLinux ? Colors.transparent : null,
body: FileManagerTabPage( body: FileManagerTabPage(
params: params, params: params,
), ),

View File

@@ -17,6 +17,7 @@ class DesktopPortForwardScreen extends StatelessWidget {
ChangeNotifierProvider.value(value: gFFI.ffiModel), ChangeNotifierProvider.value(value: gFFI.ffiModel),
], ],
child: Scaffold( child: Scaffold(
backgroundColor: isLinux ? Colors.transparent : null,
body: PortForwardTabPage( body: PortForwardTabPage(
params: params, params: params,
), ),

View File

@@ -271,7 +271,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
/// The text style of the popup menu item. /// The text style of the popup menu item.
/// ///
/// If this property is null, then [PopupMenuThemeData.textStyle] is used. /// If this property is null, then [PopupMenuThemeData.textStyle] is used.
/// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1] /// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium]
/// of [ThemeData.textTheme] is used. /// of [ThemeData.textTheme] is used.
final TextStyle? textStyle; final TextStyle? textStyle;
@@ -341,8 +341,9 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
@protected @protected
void handleTap() { void handleTap() {
widget.onTap?.call(); widget.onTap?.call();
if (Navigator.canPop(context)) {
Navigator.pop<T>(context, widget.value); Navigator.pop<T>(context, widget.value);
}
} }
@override @override
@@ -351,7 +352,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
TextStyle style = widget.textStyle ?? TextStyle style = widget.textStyle ??
popupMenuTheme.textStyle ?? popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!; theme.textTheme.titleMedium!;
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor); if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);

View File

@@ -65,7 +65,7 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
TextStyle style = widget.textStyle ?? TextStyle style = widget.textStyle ??
popupMenuTheme.textStyle ?? popupMenuTheme.textStyle ??
theme.textTheme.subtitle1!; theme.textTheme.titleMedium!;
return Obx(() => mod_menu.PopupMenuButton<T>( return Obx(() => mod_menu.PopupMenuButton<T>(
enabled: enabled.value, enabled: enabled.value,
position: widget.position, position: widget.position,
@@ -445,9 +445,18 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
dismissCallback: dismissCallback, dismissCallback: dismissCallback,
); );
bool get isEnabled => enabled?.value ?? true;
RxBool get curOption; RxBool get curOption;
Future<void> setOption(bool? option); Future<void> setOption(bool? option);
tryPop(BuildContext context) {
if (dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
super.dismissCallback?.call();
}
}
@override @override
List<mod_menu.PopupMenuEntry<T>> build( List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) { BuildContext context, MenuConfig conf) {
@@ -481,44 +490,33 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
if (switchType == SwitchType.sswitch) { if (switchType == SwitchType.sswitch) {
return Switch( return Switch(
value: curOption.value, value: curOption.value,
onChanged: (v) { onChanged: isEnabled
if (super.dismissOnClicked && ? (v) {
Navigator.canPop(context)) { tryPop(context);
Navigator.pop(context); setOption(v);
if (super.dismissCallback != null) { }
super.dismissCallback!(); : null,
}
}
setOption(v);
},
); );
} else { } else {
return Checkbox( return Checkbox(
value: curOption.value, value: curOption.value,
onChanged: (v) { onChanged: isEnabled
if (super.dismissOnClicked && ? (v) {
Navigator.canPop(context)) { tryPop(context);
Navigator.pop(context); setOption(v);
if (super.dismissCallback != null) { }
super.dismissCallback!(); : null,
}
}
setOption(v);
},
); );
} }
})), })),
)) ))
])), ])),
onPressed: () { onPressed: isEnabled
if (super.dismissOnClicked && Navigator.canPop(context)) { ? () {
Navigator.pop(context); tryPop(context);
if (super.dismissCallback != null) { setOption(!curOption.value);
super.dismissCallback!(); }
} : null,
}
setOption(!curOption.value);
},
)), )),
) )
]; ];

View File

@@ -3,10 +3,10 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/audio_input.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart'; import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
@@ -26,46 +26,42 @@ import './popup_menu.dart';
import './kb_layout_type_chooser.dart'; import './kb_layout_type_chooser.dart';
class ToolbarState { class ToolbarState {
final kStoreKey = 'remoteMenubarState';
late RxBool show;
late RxBool _pin; late RxBool _pin;
bool isShowInited = false;
RxBool show = false.obs;
ToolbarState() { ToolbarState() {
final s = bind.getLocalFlutterOption(k: kStoreKey); _pin = RxBool(false);
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
if (s.isEmpty) { if (s.isEmpty) {
_initSet(false, false);
return; return;
} }
try { try {
final m = jsonDecode(s); final m = jsonDecode(s);
if (m == null) { if (m != null) {
_initSet(false, false); _pin = RxBool(m['pin'] ?? false);
} else {
_initSet(m['pin'] ?? false, m['pin'] ?? false);
} }
} catch (e) { } catch (e) {
debugPrint('Failed to decode toolbar state ${e.toString()}'); debugPrint('Failed to decode toolbar state ${e.toString()}');
_initSet(false, false);
} }
} }
_initSet(bool s, bool p) {
// Show remubar when connection is established.
show =
RxBool(bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
_pin = RxBool(p);
}
bool get pin => _pin.value; bool get pin => _pin.value;
switchShow() async { switchShow(SessionID sessionId) async {
bind.sessionToggleOption(
sessionId: sessionId, value: kOptionCollapseToolbar);
show.value = !show.value; show.value = !show.value;
} }
setShow(bool v) async { initShow(SessionID sessionId) async {
if (show.value != v) { if (!isShowInited) {
show.value = v; show.value = !(await bind.sessionGetToggleOption(
sessionId: sessionId, arg: kOptionCollapseToolbar) ??
false);
isShowInited = true;
} }
} }
@@ -85,11 +81,7 @@ class ToolbarState {
_savePin() async { _savePin() async {
bind.setLocalFlutterOption( bind.setLocalFlutterOption(
k: kStoreKey, v: jsonEncode({'pin': _pin.value})); k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
}
save() async {
await _savePin();
} }
} }
@@ -333,8 +325,8 @@ class RemoteToolbar extends StatefulWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
final ToolbarState state; final ToolbarState state;
final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function(int, Function(bool)) onEnterOrLeaveImageSetter;
final VoidCallback onEnterOrLeaveImageCleaner; final Function(int) onEnterOrLeaveImageCleaner;
final Function(VoidCallback) setRemoteState; final Function(VoidCallback) setRemoteState;
RemoteToolbar({ RemoteToolbar({
@@ -394,7 +386,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
initialValue: 0, initialValue: 0,
); );
widget.onEnterOrLeaveImageSetter((enter) { widget.onEnterOrLeaveImageSetter(identityHashCode(this), (enter) {
if (enter) { if (enter) {
triggerAutoHide(); triggerAutoHide();
_isCursorOverImage = true; _isCursorOverImage = true;
@@ -414,12 +406,11 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
dispose() { dispose() {
super.dispose(); super.dispose();
widget.onEnterOrLeaveImageCleaner(); widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// No need to use future builder here.
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: Obx(() => show.value child: Obx(() => show.value
@@ -448,7 +439,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
sessionId: widget.ffi.sessionId, sessionId: widget.ffi.sessionId,
dragging: _dragging, dragging: _dragging,
fractionX: _fractionX, fractionX: _fractionX,
show: show, toolbarState: widget.state,
setFullscreen: _setFullscreen, setFullscreen: _setFullscreen,
setMinimize: _minimize, setMinimize: _minimize,
borderRadius: borderRadius, borderRadius: borderRadius,
@@ -588,8 +579,8 @@ class _MobileActionMenu extends StatelessWidget {
return Obx(() => _IconMenuButton( return Obx(() => _IconMenuButton(
assetName: 'assets/actions_mobile.svg', assetName: 'assets/actions_mobile.svg',
tooltip: 'Mobile Actions', tooltip: 'Mobile Actions',
onPressed: () => onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible(
ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi), !ffi.dialogManager.mobileActionsOverlayVisible.value),
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
? _ToolbarTheme.blueColor ? _ToolbarTheme.blueColor
: _ToolbarTheme.inactiveColor, : _ToolbarTheme.inactiveColor,
@@ -615,14 +606,14 @@ class _MonitorMenu extends StatelessWidget {
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y'; bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
bool get supportIndividualWindows => bool get supportIndividualWindows =>
useTextureRender && ffi.ffiModel.pi.isSupportMultiDisplay; !isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
@override @override
Widget build(BuildContext context) => showMonitorsToolbar Widget build(BuildContext context) => showMonitorsToolbar
? buildMultiMonitorMenu() ? buildMultiMonitorMenu(context)
: Obx(() => buildMonitorMenu()); : Obx(() => buildMonitorMenu(context));
Widget buildMonitorMenu() { Widget buildMonitorMenu(BuildContext context) {
final width = SimpleWrapper<double>(0); final width = SimpleWrapper<double>(0);
final monitorsIcon = final monitorsIcon =
globalMonitorsWidget(width, Colors.white, Colors.black38); globalMonitorsWidget(width, Colors.white, Colors.black38);
@@ -636,18 +627,18 @@ class _MonitorMenu extends StatelessWidget {
menuStyle: MenuStyle( menuStyle: MenuStyle(
padding: padding:
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
menuChildrenGetter: () => [buildMonitorSubmenuWidget()]); menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
} }
Widget buildMultiMonitorMenu() { Widget buildMultiMonitorMenu(BuildContext context) {
return Row(children: buildMonitorList(true)); return Row(children: buildMonitorList(context, true));
} }
Widget buildMonitorSubmenuWidget() { Widget buildMonitorSubmenuWidget(BuildContext context) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Row(children: buildMonitorList(false)), Row(children: buildMonitorList(context, false)),
supportIndividualWindows ? Divider() : Offstage(), supportIndividualWindows ? Divider() : Offstage(),
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(), supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
], ],
@@ -663,7 +654,7 @@ class _MonitorMenu extends StatelessWidget {
onChanged: (value) async { onChanged: (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionSetDisplaysAsIndividualWindows( await bind.sessionSetDisplaysAsIndividualWindows(
sessionId: ffi.sessionId, value: value ? 'Y' : ''); sessionId: ffi.sessionId, value: value ? 'Y' : 'N');
}, },
ffi: ffi, ffi: ffi,
child: Text(translate('Show displays as individual windows'))); child: Text(translate('Show displays as individual windows')));
@@ -680,7 +671,7 @@ class _MonitorMenu extends StatelessWidget {
), ),
); );
List<Widget> buildMonitorList(bool isMulti) { List<Widget> buildMonitorList(BuildContext context, bool isMulti) {
final List<Widget> monitorList = []; final List<Widget> monitorList = [];
final pi = ffi.ffiModel.pi; final pi = ffi.ffiModel.pi;
@@ -818,7 +809,11 @@ class _MonitorMenu extends StatelessWidget {
} }
RxInt display = CurrentDisplayState.find(id); RxInt display = CurrentDisplayState.find(id);
if (display.value != i) { if (display.value != i) {
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) { final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
bind.sessionGetDisplaysAsIndividualWindows(
sessionId: ffi.sessionId) ==
'Y';
if (isChooseDisplayToOpenInNewWindow) {
openMonitorInNewTabOrWindow(i, ffi.id, pi); openMonitorInNewTabOrWindow(i, ffi.id, pi);
} else { } else {
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti); openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
@@ -1045,7 +1040,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_screenAdjustor.updateScreen(); _screenAdjustor.updateScreen();
menuChildrenGetter() { menuChildrenGetter() {
final menuChildren = <Widget>[ final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context), _screenAdjustor.adjustWindow(context),
@@ -1058,11 +1052,17 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi, ffi: widget.ffi,
screenAdjustor: _screenAdjustor, screenAdjustor: _screenAdjustor,
), ),
// We may add this feature if it is needed and we have an EV certificate. if (pi.isRustDeskIdd)
// _VirtualDisplayMenu( _RustDeskVirtualDisplayMenu(
// id: widget.id, id: widget.id,
// ffi: widget.ffi, ffi: widget.ffi,
// ), ),
if (pi.isAmyuniIdd)
_AmyuniVirtualDisplayMenu(
id: widget.id,
ffi: widget.ffi,
),
cursorToggles(),
Divider(), Divider(),
toggles(), toggles(),
]; ];
@@ -1207,6 +1207,25 @@ class _DisplayMenuState extends State<_DisplayMenu> {
}); });
} }
cursorToggles() {
return futureBuilder(
future: toolbarCursor(context, id, ffi),
hasData: (data) {
final v = data as List<TToggleMenu>;
if (v.isEmpty) return Offstage();
return Column(children: [
Divider(),
...v
.map((e) => CkbMenuButton(
value: e.value,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList(),
]);
});
}
toggles() { toggles() {
return futureBuilder( return futureBuilder(
future: toolbarDisplayToggle(context, id, ffi), future: toolbarDisplayToggle(context, id, ffi),
@@ -1540,21 +1559,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
} }
} }
class _VirtualDisplayMenu extends StatefulWidget { class _RustDeskVirtualDisplayMenu extends StatefulWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
_VirtualDisplayMenu({ _RustDeskVirtualDisplayMenu({
Key? key, Key? key,
required this.id, required this.id,
required this.ffi, required this.ffi,
}) : super(key: key); }) : super(key: key);
@override @override
State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState(); State<_RustDeskVirtualDisplayMenu> createState() =>
_RustDeskVirtualDisplayMenuState();
} }
class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> { class _RustDeskVirtualDisplayMenuState
extends State<_RustDeskVirtualDisplayMenu> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -1569,7 +1590,7 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
return Offstage(); return Offstage();
} }
final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays; final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
final privacyModeState = PrivacyModeState.find(widget.id); final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[]; final children = <Widget>[];
@@ -1611,6 +1632,82 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
} }
} }
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_AmyuniVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_AmyuniVirtualDisplayMenu> createState() =>
_AmiyuniVirtualDisplayMenuState();
}
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return Offstage();
}
if (!widget.ffi.ffiModel.pi.isInstalled) {
return Offstage();
}
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: false),
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: true),
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)),
];
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,
child: Text(translate("Virtual display")),
);
}
}
class _KeyboardMenu extends StatelessWidget { class _KeyboardMenu extends StatelessWidget {
final String id; final String id;
final FFI ffi; final FFI ffi;
@@ -1773,7 +1870,7 @@ class _KeyboardMenu extends StatelessWidget {
? (value) async { ? (value) async {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption( await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionViewOnly); sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
ffiModel.setViewOnly(id, value); ffiModel.setViewOnly(id, value);
} }
: null, : null,
@@ -1852,32 +1949,70 @@ class _VoiceCallMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
menuChildrenGetter() {
final audioInput =
AudioInput(builder: (devices, currentDevice, setDevice) {
return Column(
children: devices
.map((d) => RdoMenuButton<String>(
child: Container(
child: Text(
d,
overflow: TextOverflow.ellipsis,
),
constraints: BoxConstraints(maxWidth: 250),
),
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) setDevice(v);
},
ffi: ffi,
))
.toList(),
);
});
return [
audioInput,
Divider(),
MenuButton(
child: Text(translate('End call')),
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
ffi: ffi,
),
];
}
return Obx( return Obx(
() { () {
final String tooltip;
final String icon;
switch (ffi.chatModel.voiceCallStatus.value) { switch (ffi.chatModel.voiceCallStatus.value) {
case VoiceCallStatus.waitingForResponse: case VoiceCallStatus.waitingForResponse:
tooltip = "Waiting"; return buildCallWaiting(context);
icon = "assets/call_wait.svg";
break;
case VoiceCallStatus.connected: case VoiceCallStatus.connected:
tooltip = "Disconnect"; return _IconSubmenuButton(
icon = "assets/call_end.svg"; tooltip: 'Voice call',
break; svg: 'assets/voice_call.svg',
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildrenGetter: menuChildrenGetter,
ffi: ffi,
);
default: default:
return Offstage(); return Offstage();
} }
return _IconMenuButton(
assetName: icon,
tooltip: tooltip,
onPressed: () =>
bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor);
}, },
); );
} }
Widget buildCallWaiting(BuildContext context) {
return _IconMenuButton(
assetName: "assets/call_wait.svg",
tooltip: "Waiting",
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor,
);
}
} }
class _RecordMenu extends StatelessWidget { class _RecordMenu extends StatelessWidget {
@@ -2014,7 +2149,7 @@ class _IconSubmenuButton extends StatefulWidget {
final Color hoverColor; final Color hoverColor;
final List<Widget> Function() menuChildrenGetter; final List<Widget> Function() menuChildrenGetter;
final MenuStyle? menuStyle; final MenuStyle? menuStyle;
final FFI ffi; final FFI? ffi;
final double? width; final double? width;
_IconSubmenuButton({ _IconSubmenuButton({
@@ -2025,7 +2160,7 @@ class _IconSubmenuButton extends StatefulWidget {
required this.color, required this.color,
required this.hoverColor, required this.hoverColor,
required this.menuChildrenGetter, required this.menuChildrenGetter,
required this.ffi, this.ffi,
this.menuStyle, this.menuStyle,
this.width, this.width,
}) : super(key: key); }) : super(key: key);
@@ -2107,13 +2242,13 @@ class MenuButton extends StatelessWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
final Widget? trailingIcon; final Widget? trailingIcon;
final Widget? child; final Widget? child;
final FFI ffi; final FFI? ffi;
MenuButton( MenuButton(
{Key? key, {Key? key,
this.onPressed, this.onPressed,
this.trailingIcon, this.trailingIcon,
required this.child, required this.child,
required this.ffi}) this.ffi})
: super(key: key); : super(key: key);
@override @override
@@ -2122,7 +2257,9 @@ class MenuButton extends StatelessWidget {
key: key, key: key,
onPressed: onPressed != null onPressed: onPressed != null
? () { ? () {
_menuDismissCallback(ffi); if (ffi != null) {
_menuDismissCallback(ffi!);
}
onPressed?.call(); onPressed?.call();
} }
: null, : null,
@@ -2135,13 +2272,13 @@ class CkbMenuButton extends StatelessWidget {
final bool? value; final bool? value;
final ValueChanged<bool?>? onChanged; final ValueChanged<bool?>? onChanged;
final Widget? child; final Widget? child;
final FFI ffi; final FFI? ffi;
const CkbMenuButton( const CkbMenuButton(
{Key? key, {Key? key,
required this.value, required this.value,
required this.onChanged, required this.onChanged,
required this.child, required this.child,
required this.ffi}) this.ffi})
: super(key: key); : super(key: key);
@override @override
@@ -2152,7 +2289,9 @@ class CkbMenuButton extends StatelessWidget {
child: child, child: child,
onChanged: onChanged != null onChanged: onChanged != null
? (bool? value) { ? (bool? value) {
_menuDismissCallback(ffi); if (ffi != null) {
_menuDismissCallback(ffi!);
}
onChanged?.call(value); onChanged?.call(value);
} }
: null, : null,
@@ -2165,13 +2304,13 @@ class RdoMenuButton<T> extends StatelessWidget {
final T? groupValue; final T? groupValue;
final ValueChanged<T?>? onChanged; final ValueChanged<T?>? onChanged;
final Widget? child; final Widget? child;
final FFI ffi; final FFI? ffi;
const RdoMenuButton({ const RdoMenuButton({
Key? key, Key? key,
required this.value, required this.value,
required this.groupValue, required this.groupValue,
required this.child, required this.child,
required this.ffi, this.ffi,
this.onChanged, this.onChanged,
}) : super(key: key); }) : super(key: key);
@@ -2183,7 +2322,9 @@ class RdoMenuButton<T> extends StatelessWidget {
child: child, child: child,
onChanged: onChanged != null onChanged: onChanged != null
? (T? value) { ? (T? value) {
_menuDismissCallback(ffi); if (ffi != null) {
_menuDismissCallback(ffi!);
}
onChanged?.call(value); onChanged?.call(value);
} }
: null, : null,
@@ -2195,7 +2336,7 @@ class _DraggableShowHide extends StatefulWidget {
final SessionID sessionId; final SessionID sessionId;
final RxDouble fractionX; final RxDouble fractionX;
final RxBool dragging; final RxBool dragging;
final RxBool show; final ToolbarState toolbarState;
final BorderRadius borderRadius; final BorderRadius borderRadius;
final Function(bool) setFullscreen; final Function(bool) setFullscreen;
@@ -2206,7 +2347,7 @@ class _DraggableShowHide extends StatefulWidget {
required this.sessionId, required this.sessionId,
required this.fractionX, required this.fractionX,
required this.dragging, required this.dragging,
required this.show, required this.toolbarState,
required this.setFullscreen, required this.setFullscreen,
required this.setMinimize, required this.setMinimize,
required this.borderRadius, required this.borderRadius,
@@ -2222,23 +2363,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
double left = 0.0; double left = 0.0;
double right = 1.0; double right = 1.0;
RxBool get show => widget.toolbarState.show;
@override @override
initState() { initState() {
super.initState(); super.initState();
final confLeft = double.tryParse( final confLeft = double.tryParse(
bind.mainGetLocalOption(key: 'remote-menubar-drag-left')); bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft));
if (confLeft == null) { if (confLeft == null) {
bind.mainSetLocalOption( bind.mainSetLocalOption(
key: 'remote-menubar-drag-left', value: left.toString()); key: kOptionRemoteMenubarDragLeft, value: left.toString());
} else { } else {
left = confLeft; left = confLeft;
} }
final confRight = double.tryParse( final confRight = double.tryParse(
bind.mainGetLocalOption(key: 'remote-menubar-drag-right')); bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight));
if (confRight == null) { if (confRight == null) {
bind.mainSetLocalOption( bind.mainSetLocalOption(
key: 'remote-menubar-drag-right', value: right.toString()); key: kOptionRemoteMenubarDragRight, value: right.toString());
} else { } else {
right = confRight; right = confRight;
} }
@@ -2309,28 +2452,29 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
), ),
), ),
)), )),
Obx(() => Offstage( if (!isMacOS)
offstage: isFullscreen.isFalse, Obx(() => Offstage(
child: TextButton( offstage: isFullscreen.isFalse,
onPressed: () => widget.setMinimize(), child: TextButton(
child: Tooltip( onPressed: () => widget.setMinimize(),
message: translate('Minimize'), child: Tooltip(
child: Icon( message: translate('Minimize'),
Icons.remove, child: Icon(
size: iconSize, Icons.remove,
size: iconSize,
),
), ),
), ),
), )),
)),
TextButton( TextButton(
onPressed: () => setState(() { onPressed: () => setState(() {
widget.show.value = !widget.show.value; widget.toolbarState.switchShow(widget.sessionId);
}), }),
child: Obx((() => Tooltip( child: Obx((() => Tooltip(
message: translate( message:
widget.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon( child: Icon(
widget.show.isTrue ? Icons.expand_less : Icons.expand_more, show.isTrue ? Icons.expand_less : Icons.expand_more,
size: iconSize, size: iconSize,
), ),
))), ))),
@@ -2370,10 +2514,11 @@ class InputModeMenu {
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos(); _menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
Widget _buildPointerTrackWidget(Widget child, FFI ffi) { Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
return Listener( return Listener(
onPointerHover: (PointerHoverEvent e) => onPointerHover: (PointerHoverEvent e) => {
ffi.inputModel.lastMousePos = e.position, if (ffi != null) {ffi.inputModel.lastMousePos = e.position}
},
child: MouseRegion( child: MouseRegion(
child: child, child: child,
), ),

View File

@@ -16,6 +16,7 @@ import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:scroll_pos/scroll_pos.dart'; import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../../utils/multi_window_manager.dart'; import '../../utils/multi_window_manager.dart';
@@ -256,6 +257,8 @@ class DesktopTab extends StatelessWidget {
late final DesktopTabType tabType; late final DesktopTabType tabType;
late final bool isMainWindow; late final bool isMainWindow;
final RxList<String> invisibleTabKeys = RxList.empty();
DesktopTab({ DesktopTab({
Key? key, Key? key,
required this.controller, required this.controller,
@@ -317,21 +320,12 @@ class DesktopTab extends StatelessWidget {
if (tabType != DesktopTabType.main) { if (tabType != DesktopTabType.main) {
return child; return child;
} }
return buildRemoteBlock( return buildRemoteBlock(child: child, use: canBeBlocked);
child: child,
use: () async {
var access_mode = await bind.mainGetOption(key: 'access-mode');
var option = option2bool(
'allow-remote-config-modification',
await bind.mainGetOption(
key: 'allow-remote-config-modification'));
return access_mode == 'view' || (access_mode.isEmpty && !option);
});
} }
List<Widget> _tabWidgets = []; List<Widget> _tabWidgets = [];
Widget _buildPageView() { Widget _buildPageView() {
return _buildBlock( final child = _buildBlock(
child: Obx(() => PageView( child: Obx(() => PageView(
controller: state.value.pageController, controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
@@ -355,6 +349,11 @@ class DesktopTab extends StatelessWidget {
return newList; return newList;
} }
}()))); }())));
if (tabType == DesktopTabType.remoteScreen) {
return Container(color: kColorCanvas, child: child);
} else {
return child;
}
} }
/// Check whether to show ListView /// Check whether to show ListView
@@ -388,6 +387,18 @@ class DesktopTab extends StatelessWidget {
} }
: null, : null,
onPanStart: (_) => startDragging(isMainWindow), onPanStart: (_) => startDragging(isMainWindow),
onPanCancel: () {
// We want to disable dragging of the tab area in the tab bar.
// Disable dragging is needed because macOS handles dragging by default.
if (isMacOS) {
setMovable(isMainWindow, false);
}
},
onPanEnd: (_) {
if (isMacOS) {
setMovable(isMainWindow, false);
}
},
child: Row( child: Row(
children: [ children: [
Offstage( Offstage(
@@ -430,6 +441,7 @@ class DesktopTab extends StatelessWidget {
}, },
child: _ListView( child: _ListView(
controller: controller, controller: controller,
invisibleTabKeys: invisibleTabKeys,
tabBuilder: tabBuilder, tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder, tabMenuBuilder: tabMenuBuilder,
labelGetter: labelGetter, labelGetter: labelGetter,
@@ -448,12 +460,14 @@ class DesktopTab extends StatelessWidget {
tabType: tabType, tabType: tabType,
state: state, state: state,
tabController: controller, tabController: controller,
invisibleTabKeys: invisibleTabKeys,
tail: tail, tail: tail,
showMinimize: showMinimize, showMinimize: showMinimize,
showMaximize: showMaximize, showMaximize: showMaximize,
showClose: showClose, showClose: showClose,
onClose: onWindowCloseButton, onClose: onWindowCloseButton,
) labelGetter: labelGetter,
).paddingOnly(left: 10)
], ],
); );
} }
@@ -471,17 +485,22 @@ class WindowActionPanel extends StatefulWidget {
final Widget? tail; final Widget? tail;
final Future<bool> Function()? onClose; final Future<bool> Function()? onClose;
final RxList<String> invisibleTabKeys;
final LabelGetter? labelGetter;
const WindowActionPanel( const WindowActionPanel(
{Key? key, {Key? key,
required this.isMainWindow, required this.isMainWindow,
required this.tabType, required this.tabType,
required this.state, required this.state,
required this.tabController, required this.tabController,
required this.invisibleTabKeys,
this.tail, this.tail,
this.showMinimize = true, this.showMinimize = true,
this.showMaximize = true, this.showMaximize = true,
this.showClose = true, this.showClose = true,
this.onClose}) this.onClose,
this.labelGetter})
: super(key: key); : super(key: key);
@override @override
@@ -537,6 +556,16 @@ class WindowActionPanelState extends State<WindowActionPanel>
setState(() {}); setState(() {});
} }
@override
void onWindowFocus() {
stateGlobal.isFocused.value = true;
}
@override
void onWindowBlur() {
stateGlobal.isFocused.value = false;
}
@override @override
void onWindowMinimize() { void onWindowMinimize() {
stateGlobal.setMinimized(true); stateGlobal.setMinimized(true);
@@ -620,14 +649,12 @@ class WindowActionPanelState extends State<WindowActionPanel>
} }
// macOS specific workaround, the window is not hiding when in fullscreen. // macOS specific workaround, the window is not hiding when in fullscreen.
if (isMacOS && await windowManager.isFullScreen()) { if (isMacOS && await windowManager.isFullScreen()) {
stateGlobal.closeOnFullscreen ??= true;
await windowManager.setFullScreen(false); await windowManager.setFullScreen(false);
await macOSWindowClose( await macOSWindowClose(
() async => await windowManager.isFullScreen(), () async => await windowManager.isFullScreen(),
mainWindowClose, mainWindowClose,
); );
} else { } else {
stateGlobal.closeOnFullscreen ??= false;
await mainWindowClose(); await mainWindowClose();
} }
} else { } else {
@@ -639,7 +666,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
if (await widget.onClose?.call() ?? true) { if (await widget.onClose?.call() ?? true) {
if (await controller.isFullScreen()) { if (await controller.isFullScreen()) {
stateGlobal.closeOnFullscreen ??= true;
await controller.setFullscreen(false); await controller.setFullscreen(false);
stateGlobal.setFullscreen(false, procWnd: false); stateGlobal.setFullscreen(false, procWnd: false);
await macOSWindowClose( await macOSWindowClose(
@@ -647,7 +673,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
() async => await notMainWindowClose(controller), () async => await notMainWindowClose(controller),
); );
} else { } else {
stateGlobal.closeOnFullscreen ??= false;
await notMainWindowClose(controller); await notMainWindowClose(controller);
} }
} }
@@ -658,11 +683,34 @@ class WindowActionPanelState extends State<WindowActionPanel>
super.onWindowClose(); super.onWindowClose();
} }
bool showTabDowndown() {
return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
widget.tabController.tabType == DesktopTabType.fileTransfer ||
widget.tabController.tabType == DesktopTabType.portForward ||
widget.tabController.tabType == DesktopTabType.cm);
}
List<String> existingInvisibleTab() {
return widget.invisibleTabKeys
.where((key) =>
widget.tabController.state.value.tabs.any((tab) => tab.key == key))
.toList();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Obx(() => Offstage(
offstage:
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
child: _TabDropDownButton(
controller: widget.tabController,
labelGetter: widget.labelGetter,
tabkeys: existingInvisibleTab()),
)),
Offstage(offstage: widget.tail == null, child: widget.tail), Offstage(offstage: widget.tail == null, child: widget.tail),
Offstage( Offstage(
offstage: kUseCompatibleUiMode, offstage: kUseCompatibleUiMode,
@@ -741,6 +789,14 @@ void startDragging(bool isMainWindow) {
} }
} }
void setMovable(bool isMainWindow, bool movable) {
if (isMainWindow) {
windowManager.setMovable(movable);
} else {
WindowController.fromWindowId(kWindowId!).setMovable(movable);
}
}
/// return true -> window will be maximize /// return true -> window will be maximize
/// return false -> window will be unmaximize /// return false -> window will be unmaximize
Future<bool> toggleMaximize(bool isMainWindow) async { Future<bool> toggleMaximize(bool isMainWindow) async {
@@ -768,9 +824,9 @@ Future<bool> closeConfirmDialog() async {
var confirm = true; var confirm = true;
final res = await gFFI.dialogManager.show<bool>((setState, close, context) { final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
submit() { submit() {
final opt = "enable-confirm-closing-tabs"; String value = bool2option(kOptionEnableConfirmClosingTabs, confirm);
String value = bool2option(opt, confirm); bind.mainSetLocalOption(
bind.mainSetLocalOption(key: opt, value: value); key: kOptionEnableConfirmClosingTabs, value: value);
close(true); close(true);
} }
@@ -814,6 +870,7 @@ Future<bool> closeConfirmDialog() async {
class _ListView extends StatelessWidget { class _ListView extends StatelessWidget {
final DesktopTabController controller; final DesktopTabController controller;
final RxList<String> invisibleTabKeys;
final TabBuilder? tabBuilder; final TabBuilder? tabBuilder;
final TabMenuBuilder? tabMenuBuilder; final TabMenuBuilder? tabMenuBuilder;
@@ -825,8 +882,9 @@ class _ListView extends StatelessWidget {
Rx<DesktopTabState> get state => controller.state; Rx<DesktopTabState> get state => controller.state;
const _ListView({ _ListView({
required this.controller, required this.controller,
required this.invisibleTabKeys,
this.tabBuilder, this.tabBuilder,
this.tabMenuBuilder, this.tabMenuBuilder,
this.labelGetter, this.labelGetter,
@@ -846,6 +904,19 @@ class _ListView extends StatelessWidget {
controller.tabType == DesktopTabType.install; controller.tabType == DesktopTabType.install;
} }
onVisibilityChanged(VisibilityInfo info) {
final key = (info.key as ValueKey).value;
if (info.visibleFraction < 0.75) {
if (!invisibleTabKeys.contains(key)) {
invisibleTabKeys.add(key);
}
invisibleTabKeys.removeWhere((key) =>
controller.state.value.tabs.where((e) => e.key == key).isEmpty);
} else {
invisibleTabKeys.remove(key);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() => ListView( return Obx(() => ListView(
@@ -858,35 +929,45 @@ class _ListView extends StatelessWidget {
: state.value.tabs.asMap().entries.map((e) { : state.value.tabs.asMap().entries.map((e) {
final index = e.key; final index = e.key;
final tab = e.value; final tab = e.value;
return _Tab( final label = labelGetter == null
? Rx<String>(tab.label)
: labelGetter!(tab.label);
final child = VisibilityDetector(
key: ValueKey(tab.key), key: ValueKey(tab.key),
index: index, onVisibilityChanged: onVisibilityChanged,
tabInfoKey: tab.key, child: _Tab(
label: labelGetter == null key: ValueKey(tab.key),
? Rx<String>(tab.label) index: index,
: labelGetter!(tab.label), tabInfoKey: tab.key,
selectedIcon: tab.selectedIcon, label: label,
unselectedIcon: tab.unselectedIcon, tabType: controller.tabType,
closable: tab.closable, selectedIcon: tab.selectedIcon,
selected: state.value.selected, unselectedIcon: tab.unselectedIcon,
onClose: () { closable: tab.closable,
if (tab.onTabCloseButton != null) { selected: state.value.selected,
tab.onTabCloseButton!(); onClose: () {
} else { if (tab.onTabCloseButton != null) {
controller.remove(index); tab.onTabCloseButton!();
} } else {
}, controller.remove(index);
onTap: () { }
controller.jumpTo(index); },
tab.onTap?.call(); onTap: () {
}, controller.jumpTo(index);
tabBuilder: tabBuilder, tab.onTap?.call();
tabMenuBuilder: tabMenuBuilder, },
maxLabelWidth: maxLabelWidth, tabBuilder: tabBuilder,
selectedTabBackgroundColor: selectedTabBackgroundColor ?? tabMenuBuilder: tabMenuBuilder,
MyTheme.tabbar(context).selectedTabBackgroundColor, maxLabelWidth: maxLabelWidth,
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, selectedTabBackgroundColor: selectedTabBackgroundColor ??
selectedBorderColor: selectedBorderColor, MyTheme.tabbar(context).selectedTabBackgroundColor,
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
selectedBorderColor: selectedBorderColor,
),
);
return GestureDetector(
onPanStart: (e) {},
child: child,
); );
}).toList())); }).toList()));
} }
@@ -896,6 +977,7 @@ class _Tab extends StatefulWidget {
final int index; final int index;
final String tabInfoKey; final String tabInfoKey;
final Rx<String> label; final Rx<String> label;
final DesktopTabType tabType;
final IconData? selectedIcon; final IconData? selectedIcon;
final IconData? unselectedIcon; final IconData? unselectedIcon;
final bool closable; final bool closable;
@@ -914,6 +996,7 @@ class _Tab extends StatefulWidget {
required this.index, required this.index,
required this.tabInfoKey, required this.tabInfoKey,
required this.label, required this.label,
required this.tabType,
this.selectedIcon, this.selectedIcon,
this.unselectedIcon, this.unselectedIcon,
this.tabBuilder, this.tabBuilder,
@@ -953,7 +1036,9 @@ class _TabState extends State<_Tab> with RestorationMixin {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
child: Tooltip( child: Tooltip(
message: translate(widget.label.value), message: widget.tabType == DesktopTabType.main
? ''
: translate(widget.label.value),
child: Text( child: Text(
translate(widget.label.value), translate(widget.label.value),
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -1110,6 +1195,7 @@ class ActionIcon extends StatefulWidget {
final String? message; final String? message;
final IconData icon; final IconData icon;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
final GestureTapDownCallback? onTapDown;
final bool isClose; final bool isClose;
final double iconSize; final double iconSize;
final double boxSize; final double boxSize;
@@ -1119,6 +1205,7 @@ class ActionIcon extends StatefulWidget {
this.message, this.message,
required this.icon, required this.icon,
this.onTap, this.onTap,
this.onTapDown,
this.isClose = false, this.isClose = false,
this.iconSize = _kActionIconSize, this.iconSize = _kActionIconSize,
this.boxSize = _kTabBarHeight - 1}) this.boxSize = _kTabBarHeight - 1})
@@ -1148,6 +1235,7 @@ class _ActionIconState extends State<ActionIcon> {
: MyTheme.tabbar(context).hoverColor, : MyTheme.tabbar(context).hoverColor,
onHover: (value) => hover.value = value, onHover: (value) => hover.value = value,
onTap: widget.onTap, onTap: widget.onTap,
onTapDown: widget.onTapDown,
child: SizedBox( child: SizedBox(
height: widget.boxSize, height: widget.boxSize,
width: widget.boxSize, width: widget.boxSize,
@@ -1188,6 +1276,103 @@ class AddButton extends StatelessWidget {
} }
} }
class _TabDropDownButton extends StatefulWidget {
final DesktopTabController controller;
final List<String> tabkeys;
final LabelGetter? labelGetter;
const _TabDropDownButton(
{required this.controller, required this.tabkeys, this.labelGetter});
@override
State<_TabDropDownButton> createState() => _TabDropDownButtonState();
}
class _TabDropDownButtonState extends State<_TabDropDownButton> {
var position = RelativeRect.fromLTRB(0, 0, 0, 0);
@override
Widget build(BuildContext context) {
List<String> sortedKeys = widget.controller.state.value.tabs
.where((e) => widget.tabkeys.contains(e.key))
.map((e) => e.key)
.toList();
return ActionIcon(
onTapDown: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
position = RelativeRect.fromLTRB(x, y, x, y);
},
icon: Icons.arrow_drop_down,
onTap: () {
showMenu(
context: context,
position: position,
items: sortedKeys.map((e) {
var label = e;
final tabInfo = widget.controller.state.value.tabs
.firstWhereOrNull((element) => element.key == e);
if (tabInfo != null) {
label = tabInfo.label;
}
if (widget.labelGetter != null) {
label = widget.labelGetter!(e).value;
}
var index = widget.controller.state.value.tabs
.indexWhere((t) => t.key == e);
label = '${index + 1}. $label';
final menuHover = false.obs;
final btnHover = false.obs;
return PopupMenuItem<String>(
value: e,
height: 32,
onTap: () {
widget.controller.jumpToByKey(e);
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: MouseRegion(
onHover: (event) => setState(() => menuHover.value = true),
onExit: (event) => setState(() => menuHover.value = false),
child: Row(
children: [
Expanded(
child: InkWell(child: Text(label)),
),
Obx(
() => Offstage(
offstage: !(tabInfo?.onTabCloseButton != null &&
menuHover.value),
child: InkWell(
onTap: () {
tabInfo?.onTabCloseButton?.call();
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
onHover: (event) =>
setState(() => btnHover.value = true),
onExit: (event) =>
setState(() => btnHover.value = false),
child: Icon(Icons.close,
color:
btnHover.value ? Colors.red : null))),
),
)
],
),
),
);
}).toList(),
);
},
);
}
}
class TabbarTheme extends ThemeExtension<TabbarTheme> { class TabbarTheme extends ThemeExtension<TabbarTheme> {
final Color? selectedTabIconColor; final Color? selectedTabIconColor;
final Color? unSelectedTabIconColor; final Color? unSelectedTabIconColor;

View File

@@ -6,6 +6,7 @@ import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/overlay.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart';
import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart';
@@ -95,6 +96,9 @@ Future<void> main(List<String> args) async {
desktopType = DesktopType.main; desktopType = DesktopType.main;
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
windowManager.setPreventClose(true); windowManager.setPreventClose(true);
if (isMacOS) {
disableWindowMovable(kWindowId);
}
runMainApp(true); runMainApp(true);
} }
} }
@@ -144,7 +148,8 @@ void runMainApp(bool startService) async {
} }
windowManager.setOpacity(1); windowManager.setOpacity(1);
windowManager.setTitle(getWindowName()); windowManager.setTitle(getWindowName());
windowManager.setResizable(!bind.isIncomingOnly()); // Do not use `windowManager.setResizable()` here.
setResizable(!bind.isIncomingOnly());
}); });
} }
@@ -152,6 +157,7 @@ void runMobileApp() async {
await initEnv(kAppTypeMain); await initEnv(kAppTypeMain);
if (isAndroid) androidChannelInit(); if (isAndroid) androidChannelInit();
if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath(); if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
draggablePositions.load();
await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
gFFI.userModel.refreshCurrentUser(); gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
@@ -166,9 +172,13 @@ void runMultiWindow(
final title = getWindowName(); final title = getWindowName();
// set prevent close to true, we handle close event manually // set prevent close to true, we handle close event manually
WindowController.fromWindowId(kWindowId!).setPreventClose(true); WindowController.fromWindowId(kWindowId!).setPreventClose(true);
if (isMacOS) {
disableWindowMovable(kWindowId);
}
late Widget widget; late Widget widget;
switch (appType) { switch (appType) {
case kAppTypeDesktopRemote: case kAppTypeDesktopRemote:
draggablePositions.load();
widget = DesktopRemoteScreen( widget = DesktopRemoteScreen(
params: argument, params: argument,
); );
@@ -238,7 +248,7 @@ void runConnectionManagerScreen() async {
} else { } else {
await showCmWindow(isStartup: true); await showCmWindow(isStartup: true);
} }
windowManager.setResizable(false); setResizable(false);
// Start the uni links handler and redirect links to Native, not for Flutter. // Start the uni links handler and redirect links to Native, not for Flutter.
listenUniLinks(handleByFlutter: false); listenUniLinks(handleByFlutter: false);
} }
@@ -248,7 +258,7 @@ bool _isCmReadyToShow = false;
showCmWindow({bool isStartup = false}) async { showCmWindow({bool isStartup = false}) async {
if (isStartup) { if (isStartup) {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions( WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat); size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
await windowManager.waitUntilReadyToShow(windowOptions, null); await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker(); bind.mainHideDocker();
await Future.wait([ await Future.wait([
@@ -337,12 +347,11 @@ void runInstallPage() async {
windowManager.focus(); windowManager.focus();
windowManager.setOpacity(1); windowManager.setOpacity(1);
windowManager.setAlignment(Alignment.center); // ensure windowManager.setAlignment(Alignment.center); // ensure
windowManager.setTitle(getWindowName());
}); });
} }
WindowOptions getHiddenTitleBarWindowOptions( WindowOptions getHiddenTitleBarWindowOptions(
{Size? size, bool center = false}) { {Size? size, bool center = false, bool? alwaysOnTop}) {
var defaultTitleBarStyle = TitleBarStyle.hidden; var defaultTitleBarStyle = TitleBarStyle.hidden;
// we do not hide titlebar on win7 because of the frame overflow. // we do not hide titlebar on win7 because of the frame overflow.
if (kUseCompatibleUiMode) { if (kUseCompatibleUiMode) {
@@ -354,6 +363,7 @@ WindowOptions getHiddenTitleBarWindowOptions(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
skipTaskbar: false, skipTaskbar: false,
titleBarStyle: defaultTitleBarStyle, titleBarStyle: defaultTitleBarStyle,
alwaysOnTop: alwaysOnTop,
); );
} }
@@ -440,6 +450,9 @@ class _AppState extends State<App> {
if (isDesktop && desktopType == DesktopType.main) { if (isDesktop && desktopType == DesktopType.main) {
child = keyListenerBuilder(context, child); child = keyListenerBuilder(context, child);
} }
if (isLinux) {
child = buildVirtualWindowFrame(context, child);
}
return child; return child;
}, },
), ),

View File

@@ -65,10 +65,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
}(); }();
} }
if (isAndroid) { if (isAndroid) {
Timer(const Duration(seconds: 1), () async { if (!bind.isCustomClient()) {
_updateUrl = await bind.mainGetSoftwareUpdateUrl(); Timer(const Duration(seconds: 1), () async {
if (_updateUrl.isNotEmpty) setState(() {}); _updateUrl = await bind.mainGetSoftwareUpdateUrl();
}); if (_updateUrl.isNotEmpty) setState(() {});
});
}
} }
_idController.addListener(() { _idController.addListener(() {
@@ -84,7 +86,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
slivers: [ slivers: [
SliverList( SliverList(
delegate: SliverChildListDelegate([ delegate: SliverChildListDelegate([
_buildUpdateUI(), if (!bind.isCustomClient()) _buildUpdateUI(),
_buildRemoteIDTextField(), _buildRemoteIDTextField(),
])), ])),
SliverFillRemaining( SliverFillRemaining(

View File

@@ -4,6 +4,7 @@ import 'package:flutter_hbb/mobile/pages/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 'connection_page.dart'; import 'connection_page.dart';
abstract class PageShape extends Widget { abstract class PageShape extends Widget {
@@ -25,8 +26,9 @@ class HomePageState extends State<HomePage> {
var _selectedIndex = 0; var _selectedIndex = 0;
int get selectedIndex => _selectedIndex; int get selectedIndex => _selectedIndex;
final List<PageShape> _pages = []; final List<PageShape> _pages = [];
int _chatPageTabIndex = -1;
bool get isChatPageCurrentTab => isAndroid bool get isChatPageCurrentTab => isAndroid
? _selectedIndex == 1 ? _selectedIndex == _chatPageTabIndex
: false; // change this when ios have chat page : false; // change this when ios have chat page
void refreshPages() { void refreshPages() {
@@ -43,8 +45,9 @@ class HomePageState extends State<HomePage> {
void initPages() { void initPages() {
_pages.clear(); _pages.clear();
_pages.add(ConnectionPage()); if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
if (isAndroid) { if (isAndroid && !bind.isOutgoingOnly()) {
_chatPageTabIndex = _pages.length;
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
} }
_pages.add(SettingsPage()); _pages.add(SettingsPage());
@@ -141,7 +144,7 @@ class HomePageState extends State<HomePage> {
], ],
); );
} }
return Text("RustDesk"); return Text(bind.mainGetAppNameSync());
} }
} }
@@ -154,7 +157,7 @@ class WebHomePage extends StatelessWidget {
// backgroundColor: MyTheme.grayBg, // backgroundColor: MyTheme.grayBg,
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text("RustDesk${isWeb ? " (Beta) " : ""}"), title: Text(bind.mainGetAppNameSync()),
actions: connectionPage.appBarActions, actions: connectionPage.appBarActions,
), ),
body: connectionPage, body: connectionPage,

View File

@@ -8,6 +8,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
@@ -68,7 +69,9 @@ class _RemotePageState extends State<RemotePage> {
gFFI.dialogManager gFFI.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection); .showLoading(translate('Connecting...'), onCancel: closeConnection);
}); });
WakelockPlus.enable(); if (!isWeb) {
WakelockPlus.enable();
}
_physicalFocusNode.requestFocus(); _physicalFocusNode.requestFocus();
gFFI.inputModel.listenToMouse(true); gFFI.inputModel.listenToMouse(true);
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
@@ -77,16 +80,19 @@ class _RemotePageState extends State<RemotePage> {
initSharedStates(widget.id); 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
Future<void> dispose() async { Future<void> dispose() async {
// https://github.com/flutter/flutter/issues/64935 // https://github.com/flutter/flutter/issues/64935
super.dispose(); super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay(); gFFI.dialogManager.hideMobileActionsOverlay(store: false);
gFFI.inputModel.listenToMouse(false); gFFI.inputModel.listenToMouse(false);
gFFI.imageModel.disposeImage();
gFFI.cursorModel.disposeImages();
await gFFI.invokeMethod("enable_soft_keyboard", true); await gFFI.invokeMethod("enable_soft_keyboard", true);
_mobileFocusNode.dispose(); _mobileFocusNode.dispose();
_physicalFocusNode.dispose(); _physicalFocusNode.dispose();
@@ -95,9 +101,15 @@ class _RemotePageState extends State<RemotePage> {
gFFI.dialogManager.dismissAll(); gFFI.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values); overlays: SystemUiOverlay.values);
await WakelockPlus.disable(); if (!isWeb) {
await WakelockPlus.disable();
}
await keyboardSubscription.cancel(); await keyboardSubscription.cancel();
removeSharedStates(widget.id); removeSharedStates(widget.id);
// `on_voice_call_closed` should be called when the connection is ended.
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
// Only one client is considered here for now.
gFFI.chatModel.onVoiceCallClosed("End connetion");
} }
// to-do: It should be better to use transparent color instead of the bgColor. // to-do: It should be better to use transparent color instead of the bgColor.
@@ -219,7 +231,7 @@ class _RemotePageState extends State<RemotePage> {
_timer?.cancel(); _timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboard, () { _timer = Timer(kMobileDelaySoftKeyboard, () {
// show now, and sleep a while to requestFocus to // show now, and sleep a while to requestFocus to
// make sure edit ready, so that keyboard wont show/hide/show/hide happen // make sure edit ready, so that keyboard won't show/hide/show/hide happen
setState(() => _showEdit = true); setState(() => _showEdit = true);
_timer?.cancel(); _timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboardFocus, () { _timer = Timer(kMobileDelaySoftKeyboardFocus, () {
@@ -300,7 +312,7 @@ class _RemotePageState extends State<RemotePage> {
initialEntries: [ initialEntries: [
OverlayEntry(builder: (context) { OverlayEntry(builder: (context) {
return Container( return Container(
color: Colors.black, color: kColorCanvas,
child: isWebDesktop child: isWebDesktop
? getBodyForDesktopWithListener(keyboard) ? getBodyForDesktopWithListener(keyboard)
: SafeArea( : SafeArea(
@@ -365,9 +377,7 @@ class _RemotePageState extends State<RemotePage> {
onPressed: () { onPressed: () {
clientClose(sessionId, gFFI.dialogManager); clientClose(sessionId, gFFI.dialogManager);
}, },
) ),
] +
<Widget>[
IconButton( IconButton(
color: Colors.white, color: Colors.white,
icon: Icon(Icons.tv), icon: Icon(Icons.tv),
@@ -409,15 +419,21 @@ class _RemotePageState extends State<RemotePage> {
(isWeb (isWeb
? [] ? []
: <Widget>[ : <Widget>[
IconButton( futureBuilder(
color: Colors.white, future: gFFI.invokeMethod(
icon: Icon(Icons.message), "get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
onPressed: () { hasData: (isSupportVoiceCall) => IconButton(
gFFI.chatModel.changeCurrentKey(MessageKey( color: Colors.white,
widget.id, ChatModel.clientModeID)); icon: isAndroid && isSupportVoiceCall
gFFI.chatModel.toggleChatOverlay(); ? SvgPicture.asset('assets/chat.svg',
}, colorFilter: ColorFilter.mode(
) Colors.white, BlendMode.srcIn))
: Icon(Icons.message),
onPressed: () =>
isAndroid && isSupportVoiceCall
? showChatOptions(widget.id)
: onPressedTextChat(widget.id),
))
]) + ]) +
[ [
IconButton( IconButton(
@@ -467,7 +483,11 @@ class _RemotePageState extends State<RemotePage> {
: TextFormField( : TextFormField(
textInputAction: TextInputAction.newline, textInputAction: TextInputAction.newline,
autocorrect: false, autocorrect: false,
enableSuggestions: false, // Flutter 3.16.9 Android.
// `enableSuggestions` causes secure keyboard to be shown.
// https://github.com/flutter/flutter/issues/139143
// https://github.com/flutter/flutter/issues/146540
// enableSuggestions: false,
autofocus: true, autofocus: true,
focusNode: _mobileFocusNode, focusNode: _mobileFocusNode,
maxLines: null, maxLines: null,
@@ -534,6 +554,88 @@ class _RemotePageState extends State<RemotePage> {
}(); }();
} }
onPressedTextChat(String id) {
gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
gFFI.chatModel.toggleChatOverlay();
}
showChatOptions(String id) async {
onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId);
onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
makeTextMenu(String label, Widget icon, VoidCallback onPressed,
{TextStyle? labelStyle}) =>
TTextMenu(
child: Text(translate(label), style: labelStyle),
trailingIcon: Transform.scale(
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
child: IconButton(
onPressed: onPressed,
icon: icon,
),
),
onPressed: onPressed,
);
final isInVoice = [
VoiceCallStatus.waitingForResponse,
VoiceCallStatus.connected
].contains(gFFI.chatModel.voiceCallStatus.value);
final menus = [
makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
() => onPressedTextChat(widget.id)),
isInVoice
? makeTextMenu(
'End voice call',
SvgPicture.asset(
'assets/call_wait.svg',
colorFilter:
ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
),
onPressEndVoiceCall,
labelStyle: TextStyle(color: Colors.redAccent))
: makeTextMenu(
'Voice call',
SvgPicture.asset(
'assets/call_wait.svg',
colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
),
onPressVoiceCall),
];
getChild(TTextMenu menu) {
if (menu.trailingIcon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
menu.child,
menu.trailingIcon!,
]);
} else {
return menu.child;
}
}
final menuItems = menus
.asMap()
.entries
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
.toList();
Future.delayed(Duration.zero, () async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height;
var index = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: menuItems,
elevation: 8,
);
if (index != null && index < menus.length) {
menus[index].onPressed.call();
}
});
}
/// aka changeTouchMode /// aka changeTouchMode
BottomAppBar getGestureHelp() { BottomAppBar getGestureHelp() {
return BottomAppBar( return BottomAppBar(
@@ -546,7 +648,7 @@ class _RemotePageState extends State<RemotePage> {
gFFI.ffiModel.toggleTouchMode(); gFFI.ffiModel.toggleTouchMode();
final v = gFFI.ffiModel.touchMode ? 'Y' : ''; final v = gFFI.ffiModel.touchMode ? 'Y' : '';
bind.sessionPeerOption( bind.sessionPeerOption(
sessionId: sessionId, name: "touch-mode", value: v); sessionId: sessionId, name: kOptionTouchMode, value: v);
}))); })));
} }
@@ -585,6 +687,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
var _fn = false; var _fn = false;
var _pin = false; var _pin = false;
final _keyboardVisibilityController = KeyboardVisibilityController(); final _keyboardVisibilityController = KeyboardVisibilityController();
final _key = GlobalKey();
InputModel get inputModel => gFFI.inputModel; InputModel get inputModel => gFFI.inputModel;
@@ -609,6 +712,24 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
onPressed: onPressed); onPressed: onPressed);
} }
@override
void initState() {
super.initState();
}
_updateRect() {
RenderObject? renderObject = _key.currentContext?.findRenderObject();
if (renderObject == null) {
return;
}
if (renderObject is RenderBox) {
final size = renderObject.size;
Offset pos = renderObject.localToGlobal(Offset.zero);
gFFI.cursorModel.keyHelpToolsVisibilityChanged(
Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height));
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasModifierOn = inputModel.ctrl || final hasModifierOn = inputModel.ctrl ||
@@ -617,6 +738,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
inputModel.command; inputModel.command;
if (!_pin && !hasModifierOn && !widget.requestShow) { if (!_pin && !hasModifierOn && !widget.requestShow) {
gFFI.cursorModel.keyHelpToolsVisibilityChanged(null);
return Offstage(); return Offstage();
} }
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
@@ -727,7 +849,12 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
}), }),
]; ];
final space = size.width > 320 ? 4.0 : 2.0; final space = size.width > 320 ? 4.0 : 2.0;
// 500 ms is long enough for this widget to be built!
Future.delayed(Duration(milliseconds: 500), () {
_updateRect();
});
return Container( return Container(
key: _key,
color: Color(0xAA000000), color: Color(0xAA000000),
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8), top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
@@ -807,7 +934,7 @@ void showOptions(
border: Border.all(color: Theme.of(context).hintColor), border: Border.all(color: Theme.of(context).hintColor),
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
color: i == cur color: i == cur
? Theme.of(context).toggleableActiveColor.withOpacity(0.6) ? Theme.of(context).primaryColor.withOpacity(0.6)
: null), : null),
child: Center( child: Center(
child: Text((i + 1).toString(), child: Text((i + 1).toString(),
@@ -832,6 +959,7 @@ void showOptions(
List<TRadioMenu<String>> imageQualityRadios = List<TRadioMenu<String>> imageQualityRadios =
await toolbarImageQuality(context, id, gFFI); await toolbarImageQuality(context, id, gFFI);
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI); List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
List<TToggleMenu> displayToggles = List<TToggleMenu> displayToggles =
await toolbarDisplayToggle(context, id, gFFI); await toolbarDisplayToggle(context, id, gFFI);
@@ -872,8 +1000,23 @@ void showOptions(
})), })),
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
]; ];
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
final cursorTogglesList = cursorToggles
.asMap()
.entries
.map((e) => Obx(() => CheckboxListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxCursorToggleValues[e.key].value,
onChanged: (v) {
e.value.onChanged?.call(v);
if (v != null) rxCursorToggleValues[e.key].value = v;
},
title: e.value.child)))
.toList();
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList(); final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
final toggles = displayToggles final displayTogglesList = displayToggles
.asMap() .asMap()
.entries .entries
.map((e) => Obx(() => CheckboxListTile( .map((e) => Obx(() => CheckboxListTile(
@@ -886,6 +1029,11 @@ void showOptions(
}, },
title: e.value.child))) title: e.value.child)))
.toList(); .toList();
final toggles = [
...cursorTogglesList,
if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border),
...displayTogglesList,
];
Widget privacyModeWidget = Offstage(); Widget privacyModeWidget = Offstage();
if (privacyModeList.length > 1) { if (privacyModeList.length > 1) {

View File

@@ -39,6 +39,7 @@ class ServerPage extends StatefulWidget implements PageShape {
final approveMode = gFFI.serverModel.approveMode; final approveMode = gFFI.serverModel.approveMode;
final verificationMethod = gFFI.serverModel.verificationMethod; final verificationMethod = gFFI.serverModel.verificationMethod;
final showPasswordOption = approveMode != 'click'; final showPasswordOption = approveMode != 'click';
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
return [ return [
PopupMenuItem( PopupMenuItem(
enabled: gFFI.serverModel.connectStatus > 0, enabled: gFFI.serverModel.connectStatus > 0,
@@ -50,16 +51,19 @@ class ServerPage extends StatefulWidget implements PageShape {
value: 'AcceptSessionsViaPassword', value: 'AcceptSessionsViaPassword',
child: listTile( child: listTile(
'Accept sessions via password', approveMode == 'password'), 'Accept sessions via password', approveMode == 'password'),
enabled: !isApproveModeFixed,
), ),
PopupMenuItem( PopupMenuItem(
value: 'AcceptSessionsViaClick', value: 'AcceptSessionsViaClick',
child: child:
listTile('Accept sessions via click', approveMode == 'click'), listTile('Accept sessions via click', approveMode == 'click'),
enabled: !isApproveModeFixed,
), ),
PopupMenuItem( PopupMenuItem(
value: "AcceptSessionsViaBoth", value: "AcceptSessionsViaBoth",
child: listTile("Accept sessions via both", child: listTile("Accept sessions via both",
approveMode != 'password' && approveMode != 'click'), approveMode != 'password' && approveMode != 'click'),
enabled: !isApproveModeFixed,
), ),
if (showPasswordOption) const PopupMenuDivider(), if (showPasswordOption) const PopupMenuDivider(),
if (showPasswordOption && if (showPasswordOption &&
@@ -107,7 +111,7 @@ class ServerPage extends StatefulWidget implements PageShape {
} else if (value == kUsePermanentPassword || } else if (value == kUsePermanentPassword ||
value == kUseTemporaryPassword || value == kUseTemporaryPassword ||
value == kUseBothPasswords) { value == kUseBothPasswords) {
bind.mainSetOption(key: "verification-method", value: value); bind.mainSetOption(key: kOptionVerificationMethod, value: value);
gFFI.serverModel.updatePasswordModel(); gFFI.serverModel.updatePasswordModel();
} else if (value.startsWith("AcceptSessionsVia")) { } else if (value.startsWith("AcceptSessionsVia")) {
value = value.substring("AcceptSessionsVia".length); value = value.substring("AcceptSessionsVia".length);
@@ -116,7 +120,7 @@ class ServerPage extends StatefulWidget implements PageShape {
} else if (value == "Click") { } else if (value == "Click") {
gFFI.serverModel.setApproveMode('click'); gFFI.serverModel.setApproveMode('click');
} else { } else {
gFFI.serverModel.setApproveMode(''); gFFI.serverModel.setApproveMode(defaultOptionApproveMode);
} }
} }
}) })
@@ -158,6 +162,7 @@ class _ServerPageState extends State<ServerPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
buildPresetPasswordWarning(),
gFFI.serverModel.isStart gFFI.serverModel.isStart
? ServerInfo() ? ServerInfo()
: ServiceNotRunningNotification(), : ServiceNotRunningNotification(),
@@ -226,7 +231,7 @@ class ScamWarningDialog extends StatefulWidget {
} }
class ScamWarningDialogState extends State<ScamWarningDialog> { class ScamWarningDialogState extends State<ScamWarningDialog> {
int _countdown = 12; int _countdown = bind.isCustomClient() ? 0 : 12;
bool show_warning = false; bool show_warning = false;
late Timer _timer; late Timer _timer;
late ServerModel _serverModel; late ServerModel _serverModel;
@@ -636,40 +641,94 @@ class ConnectionManager extends StatelessWidget {
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
).marginOnly(bottom: 5), ).marginOnly(bottom: 5),
client.authorized client.authorized
? Container( ? _buildDisconnectButton(client)
alignment: Alignment.centerRight, : _buildNewConnectionHint(serverModel, client),
child: ElevatedButton.icon( if (client.incomingVoiceCall && !client.inVoiceCall)
style: ButtonStyle( ..._buildNewVoiceCallHint(context, serverModel, client),
backgroundColor:
MaterialStatePropertyAll(Colors.red)),
icon: const Icon(Icons.close),
onPressed: () {
bind.cmCloseConnection(connId: client.id);
gFFI.invokeMethod(
"cancel_notification", client.id);
},
label: Text(translate("Disconnect"))))
: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
child: Text(translate("Dismiss")),
onPressed: () {
serverModel.sendLoginResponse(
client, false);
}).marginOnly(right: 15),
if (serverModel.approveMode != 'password')
ElevatedButton.icon(
icon: const Icon(Icons.check),
label: Text(translate("Accept")),
onPressed: () {
serverModel.sendLoginResponse(
client, true);
}),
]),
]))) ])))
.toList()); .toList());
} }
Widget _buildDisconnectButton(Client client) {
final disconnectButton = ElevatedButton.icon(
style: ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.red)),
icon: const Icon(Icons.close),
onPressed: () {
bind.cmCloseConnection(connId: client.id);
gFFI.invokeMethod("cancel_notification", client.id);
},
label: Text(translate("Disconnect")),
);
final buttons = [disconnectButton];
if (client.inVoiceCall) {
buttons.insert(
0,
ElevatedButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red)),
icon: const Icon(Icons.phone),
label: Text(translate("Stop")),
onPressed: () {
bind.cmCloseVoiceCall(id: client.id);
gFFI.invokeMethod("cancel_notification", client.id);
},
),
);
}
if (buttons.length == 1) {
return Container(
alignment: Alignment.centerRight,
child: disconnectButton,
);
} else {
return Row(
children: buttons,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
);
}
}
Widget _buildNewConnectionHint(ServerModel serverModel, Client client) {
return Row(mainAxisAlignment: MainAxisAlignment.end, children: [
TextButton(
child: Text(translate("Dismiss")),
onPressed: () {
serverModel.sendLoginResponse(client, false);
}).marginOnly(right: 15),
if (serverModel.approveMode != 'password')
ElevatedButton.icon(
icon: const Icon(Icons.check),
label: Text(translate("Accept")),
onPressed: () {
serverModel.sendLoginResponse(client, true);
}),
]);
}
List<Widget> _buildNewVoiceCallHint(
BuildContext context, ServerModel serverModel, Client client) {
return [
Text(
translate("android_new_voice_call_tip"),
style: Theme.of(context).textTheme.bodyMedium,
).marginOnly(bottom: 5),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
TextButton(
child: Text(translate("Dismiss")),
onPressed: () {
serverModel.handleVoiceCall(client, false);
}).marginOnly(right: 15),
if (serverModel.approveMode != 'password')
ElevatedButton.icon(
icon: const Icon(Icons.check),
label: Text(translate("Accept")),
onPressed: () {
serverModel.handleVoiceCall(client, true);
}),
])
];
}
} }
class PaddingCard extends StatelessWidget { class PaddingCard extends StatelessWidget {
@@ -786,6 +845,24 @@ void androidChannelInit() {
gFFI.serverModel.stopService(); gFFI.serverModel.stopService();
break; break;
} }
case "msgbox":
{
var type = arguments["type"] as String;
var title = arguments["title"] as String;
var text = arguments["text"] as String;
var link = (arguments["link"] ?? '') as String;
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
break;
}
case "stop_service":
{
print(
"stop_service by kotlin, isStart:${gFFI.serverModel.isStart}");
if (gFFI.serverModel.isStart) {
gFFI.serverModel.stopService();
}
break;
}
} }
} catch (e) { } catch (e) {
debugPrintStack(label: "MethodCallHandler err:$e"); debugPrintStack(label: "MethodCallHandler err:$e");

View File

@@ -27,7 +27,7 @@ class SettingsPage extends StatefulWidget implements PageShape {
final icon = Icon(Icons.settings); final icon = Icon(Icons.settings);
@override @override
final appBarActions = [ScanButton()]; final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()];
@override @override
State<SettingsPage> createState() => _SettingsState(); State<SettingsPage> createState() => _SettingsState();
@@ -35,15 +35,47 @@ class SettingsPage extends StatefulWidget implements PageShape {
const url = 'https://rustdesk.com/'; const url = 'https://rustdesk.com/';
enum KeepScreenOn {
never,
duringControlled,
serviceOn,
}
String _keepScreenOnToOption(KeepScreenOn value) {
switch (value) {
case KeepScreenOn.never:
return 'never';
case KeepScreenOn.duringControlled:
return 'during-controlled';
case KeepScreenOn.serviceOn:
return 'service-on';
}
}
KeepScreenOn optionToKeepScreenOn(String value) {
switch (value) {
case 'never':
return KeepScreenOn.never;
case 'service-on':
return KeepScreenOn.serviceOn;
default:
return KeepScreenOn.duringControlled;
}
}
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver { class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
final _hasIgnoreBattery = androidVersion >= 26; final _hasIgnoreBattery =
false; //androidVersion >= 26; // remove because not work on every device
var _ignoreBatteryOpt = false; var _ignoreBatteryOpt = false;
var _enableStartOnBoot = false; var _enableStartOnBoot = false;
var _floatingWindowDisabled = false;
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
var _enableAbr = false; var _enableAbr = false;
var _denyLANDiscovery = false; var _denyLANDiscovery = false;
var _onlyWhiteList = false; var _onlyWhiteList = false;
var _enableDirectIPAccess = false; var _enableDirectIPAccess = false;
var _enableRecordSession = false; var _enableRecordSession = false;
var _enableHardwareCodec = false;
var _autoRecordIncomingSession = false; var _autoRecordIncomingSession = false;
var _allowAutoDisconnect = false; var _allowAutoDisconnect = false;
var _localIP = ""; var _localIP = "";
@@ -57,6 +89,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_enableAbr = option2bool(
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
_onlyWhiteList = (bind.mainGetOptionSync(key: kOptionWhitelist)) !=
defaultOptionWhitelist;
_enableDirectIPAccess = option2bool(
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
_enableRecordSession = option2bool(kOptionEnableRecordSession,
bind.mainGetOptionSync(key: kOptionEnableRecordSession));
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
_autoDisconnectTimeout =
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
() async { () async {
var update = false; var update = false;
@@ -85,60 +138,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
_enableStartOnBoot = enableStartOnBoot; _enableStartOnBoot = enableStartOnBoot;
} }
final enableAbrRes = option2bool( var floatingWindowDisabled =
"enable-abr", await bind.mainGetOption(key: "enable-abr")); bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
if (enableAbrRes != _enableAbr) { !await AndroidPermissionManager.check(kSystemAlertWindow);
if (floatingWindowDisabled != _floatingWindowDisabled) {
update = true; update = true;
_enableAbr = enableAbrRes; _floatingWindowDisabled = floatingWindowDisabled;
} }
final denyLanDiscovery = !option2bool('enable-lan-discovery', final keepScreenOn = _floatingWindowDisabled
await bind.mainGetOption(key: 'enable-lan-discovery')); ? KeepScreenOn.never
if (denyLanDiscovery != _denyLANDiscovery) { : optionToKeepScreenOn(
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
if (keepScreenOn != _keepScreenOn) {
update = true; update = true;
_denyLANDiscovery = denyLanDiscovery; _keepScreenOn = keepScreenOn;
}
final onlyWhiteList =
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
if (onlyWhiteList != _onlyWhiteList) {
update = true;
_onlyWhiteList = onlyWhiteList;
}
final enableDirectIPAccess = option2bool(
'direct-server', await bind.mainGetOption(key: 'direct-server'));
if (enableDirectIPAccess != _enableDirectIPAccess) {
update = true;
_enableDirectIPAccess = enableDirectIPAccess;
}
final enableRecordSession = option2bool('enable-record-session',
await bind.mainGetOption(key: 'enable-record-session'));
if (enableRecordSession != _enableRecordSession) {
update = true;
_enableRecordSession = enableRecordSession;
}
final autoRecordIncomingSession = option2bool(
'allow-auto-record-incoming',
await bind.mainGetOption(key: 'allow-auto-record-incoming'));
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
update = true;
_autoRecordIncomingSession = autoRecordIncomingSession;
}
final localIP = await bind.mainGetOption(key: 'local-ip-addr');
if (localIP != _localIP) {
update = true;
_localIP = localIP;
}
final directAccessPort =
await bind.mainGetOption(key: 'direct-access-port');
if (directAccessPort != _directAccessPort) {
update = true;
_directAccessPort = directAccessPort;
} }
final fingerprint = await bind.mainGetFingerprint(); final fingerprint = await bind.mainGetFingerprint();
@@ -152,21 +166,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
update = true; update = true;
_buildDate = buildDate; _buildDate = buildDate;
} }
final allowAutoDisconnect = option2bool('allow-auto-disconnect',
await bind.mainGetOption(key: 'allow-auto-disconnect'));
if (allowAutoDisconnect != _allowAutoDisconnect) {
update = true;
_allowAutoDisconnect = allowAutoDisconnect;
}
final autoDisconnectTimeout =
await bind.mainGetOption(key: 'auto-disconnect-timeout');
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
update = true;
_autoDisconnectTimeout = autoDisconnectTimeout;
}
if (update) { if (update) {
setState(() {}); setState(() {});
} }
@@ -218,6 +217,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Provider.of<FfiModel>(context); Provider.of<FfiModel>(context);
final outgoingOnly = bind.isOutgoingOnly();
final customClientSection = CustomSettingsSection(
child: Column(
children: [
if (bind.isCustomClient())
Align(
alignment: Alignment.center,
child: loadPowered(context),
),
Align(
alignment: Alignment.center,
child: loadLogo(),
)
],
));
final List<AbstractSettingsTile> enhancementsTiles = []; final List<AbstractSettingsTile> enhancementsTiles = [];
final List<AbstractSettingsTile> shareScreenTiles = [ final List<AbstractSettingsTile> shareScreenTiles = [
SettingsTile.switchTile( SettingsTile.switchTile(
@@ -234,16 +248,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('Deny LAN discovery')), title: Text(translate('Deny LAN discovery')),
initialValue: _denyLANDiscovery, initialValue: _denyLANDiscovery,
onToggle: (v) async { onToggle: isOptionFixed(kOptionEnableLanDiscovery)
await bind.mainSetOption( ? null
key: "enable-lan-discovery", : (v) async {
value: bool2option("enable-lan-discovery", !v)); await bind.mainSetOption(
final newValue = !option2bool('enable-lan-discovery', key: kOptionEnableLanDiscovery,
await bind.mainGetOption(key: 'enable-lan-discovery')); value: bool2option(kOptionEnableLanDiscovery, !v));
setState(() { final newValue = !option2bool(kOptionEnableLanDiscovery,
_denyLANDiscovery = newValue; await bind.mainGetOption(key: kOptionEnableLanDiscovery));
}); setState(() {
}, _denyLANDiscovery = newValue;
});
},
), ),
SettingsTile.switchTile( SettingsTile.switchTile(
title: Row(children: [ title: Row(children: [
@@ -258,7 +274,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onToggle: (_) async { onToggle: (_) async {
update() async { update() async {
final onlyWhiteList = final onlyWhiteList =
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty; (await bind.mainGetOption(key: kOptionWhitelist)) !=
defaultOptionWhitelist;
if (onlyWhiteList != _onlyWhiteList) { if (onlyWhiteList != _onlyWhiteList) {
setState(() { setState(() {
_onlyWhiteList = onlyWhiteList; _onlyWhiteList = onlyWhiteList;
@@ -272,26 +289,29 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text('${translate('Adaptive bitrate')} (beta)'), title: Text('${translate('Adaptive bitrate')} (beta)'),
initialValue: _enableAbr, initialValue: _enableAbr,
onToggle: (v) async { onToggle: isOptionFixed(kOptionEnableAbr)
await bind.mainSetOption(key: "enable-abr", value: v ? "" : "N"); ? null
final newValue = await bind.mainGetOption(key: "enable-abr") != "N"; : (v) async {
setState(() { await mainSetBoolOption(kOptionEnableAbr, v);
_enableAbr = newValue; final newValue = await mainGetBoolOption(kOptionEnableAbr);
}); setState(() {
}, _enableAbr = newValue;
});
},
), ),
SettingsTile.switchTile( SettingsTile.switchTile(
title: Text(translate('Enable recording session')), title: Text(translate('Enable recording session')),
initialValue: _enableRecordSession, initialValue: _enableRecordSession,
onToggle: (v) async { onToggle: isOptionFixed(kOptionEnableRecordSession)
await bind.mainSetOption( ? null
key: "enable-record-session", value: v ? "" : "N"); : (v) async {
final newValue = await mainSetBoolOption(kOptionEnableRecordSession, v);
await bind.mainGetOption(key: "enable-record-session") != "N"; final newValue =
setState(() { await mainGetBoolOption(kOptionEnableRecordSession);
_enableRecordSession = newValue; setState(() {
}); _enableRecordSession = newValue;
}, });
},
), ),
SettingsTile.switchTile( SettingsTile.switchTile(
title: Row( title: Row(
@@ -318,21 +338,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
Icons.edit, Icons.edit,
size: 20, size: 20,
), ),
onPressed: () async { onPressed: isOptionFixed(kOptionDirectAccessPort)
final port = await changeDirectAccessPort( ? null
_localIP, _directAccessPort); : () async {
setState(() { final port = await changeDirectAccessPort(
_directAccessPort = port; _localIP, _directAccessPort);
}); setState(() {
})) _directAccessPort = port;
});
}))
]), ]),
initialValue: _enableDirectIPAccess, initialValue: _enableDirectIPAccess,
onToggle: (_) async { onToggle: isOptionFixed(kOptionDirectServer)
_enableDirectIPAccess = !_enableDirectIPAccess; ? null
String value = bool2option('direct-server', _enableDirectIPAccess); : (_) async {
await bind.mainSetOption(key: 'direct-server', value: value); _enableDirectIPAccess = !_enableDirectIPAccess;
setState(() {}); String value =
}, bool2option(kOptionDirectServer, _enableDirectIPAccess);
await bind.mainSetOption(
key: kOptionDirectServer, value: value);
setState(() {});
},
), ),
SettingsTile.switchTile( SettingsTile.switchTile(
title: Row( title: Row(
@@ -359,22 +385,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
Icons.edit, Icons.edit,
size: 20, size: 20,
), ),
onPressed: () async { onPressed: isOptionFixed(kOptionAutoDisconnectTimeout)
final timeout = await changeAutoDisconnectTimeout( ? null
_autoDisconnectTimeout); : () async {
setState(() { final timeout = await changeAutoDisconnectTimeout(
_autoDisconnectTimeout = timeout; _autoDisconnectTimeout);
}); setState(() {
})) _autoDisconnectTimeout = timeout;
});
}))
]), ]),
initialValue: _allowAutoDisconnect, initialValue: _allowAutoDisconnect,
onToggle: (_) async { onToggle: isOptionFixed(kOptionAllowAutoDisconnect)
_allowAutoDisconnect = !_allowAutoDisconnect; ? null
String value = : (_) async {
bool2option('allow-auto-disconnect', _allowAutoDisconnect); _allowAutoDisconnect = !_allowAutoDisconnect;
await bind.mainSetOption(key: 'allow-auto-disconnect', value: value); String value = bool2option(
setState(() {}); kOptionAllowAutoDisconnect, _allowAutoDisconnect);
}, await bind.mainSetOption(
key: kOptionAllowAutoDisconnect, value: value);
setState(() {});
},
) )
]; ];
if (_hasIgnoreBattery) { if (_hasIgnoreBattery) {
@@ -448,33 +479,87 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue); gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
})); }));
return SettingsList( onFloatingWindowChanged(bool toValue) async {
if (toValue) {
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
return;
}
}
}
final disable = !toValue;
bind.mainSetLocalOption(
key: kOptionDisableFloatingWindow,
value: disable ? 'Y' : defaultOptionNo);
setState(() => _floatingWindowDisabled = disable);
gFFI.serverModel.androidUpdatekeepScreenOn();
}
enhancementsTiles.add(SettingsTile.switchTile(
initialValue: !_floatingWindowDisabled,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(translate('Floating window')),
Text('* ${translate('floating_window_tip')}',
style: Theme.of(context).textTheme.bodySmall),
]),
onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
? null
: onFloatingWindowChanged));
enhancementsTiles.add(_getPopupDialogRadioEntry(
title: 'Keep screen on',
list: [
_RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
_RadioEntry('During controlled',
_keepScreenOnToOption(KeepScreenOn.duringControlled)),
_RadioEntry('During service is on',
_keepScreenOnToOption(KeepScreenOn.serviceOn)),
],
getter: () => _keepScreenOnToOption(_floatingWindowDisabled
? KeepScreenOn.never
: optionToKeepScreenOn(
bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
? null
: (value) async {
await bind.mainSetLocalOption(
key: kOptionKeepScreenOn, value: value);
setState(() => _keepScreenOn = optionToKeepScreenOn(value));
gFFI.serverModel.androidUpdatekeepScreenOn();
},
));
final disabledSettings = bind.isDisableSettings();
final settings = SettingsList(
sections: [ sections: [
SettingsSection( customClientSection,
title: Text(translate('Account')), if (!bind.isDisableAccount())
tiles: [ SettingsSection(
SettingsTile( title: Text(translate('Account')),
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty tiles: [
? translate('Login') SettingsTile(
: '${translate('Logout')} (${gFFI.userModel.userName.value})')), title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
leading: Icon(Icons.person), ? translate('Login')
onPressed: (context) { : '${translate('Logout')} (${gFFI.userModel.userName.value})')),
if (gFFI.userModel.userName.value.isEmpty) { leading: Icon(Icons.person),
loginDialog(); onPressed: (context) {
} else { if (gFFI.userModel.userName.value.isEmpty) {
logOutConfirmDialog(); loginDialog();
} } else {
}, logOutConfirmDialog();
), }
], },
), ),
],
),
SettingsSection(title: Text(translate("Settings")), tiles: [ SettingsSection(title: Text(translate("Settings")), tiles: [
SettingsTile( if (!disabledSettings)
title: Text(translate('ID/Relay Server')), SettingsTile(
leading: Icon(Icons.cloud), title: Text(translate('ID/Relay Server')),
onPressed: (context) { leading: Icon(Icons.cloud),
showServerSettings(gFFI.dialogManager); onPressed: (context) {
}), showServerSettings(gFFI.dialogManager);
}),
SettingsTile( SettingsTile(
title: Text(translate('Language')), title: Text(translate('Language')),
leading: Icon(Icons.translate), leading: Icon(Icons.translate),
@@ -484,8 +569,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
SettingsTile( SettingsTile(
title: Text(translate( title: Text(translate(
Theme.of(context).brightness == Brightness.light Theme.of(context).brightness == Brightness.light
? 'Dark Theme' ? 'Light Theme'
: 'Light Theme')), : 'Dark Theme')),
leading: Icon(Theme.of(context).brightness == Brightness.light leading: Icon(Theme.of(context).brightness == Brightness.light
? Icons.dark_mode ? Icons.dark_mode
: Icons.light_mode), : Icons.light_mode),
@@ -495,6 +580,23 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
) )
]), ]),
if (isAndroid) if (isAndroid)
SettingsSection(title: Text(translate('Hardware Codec')), tiles: [
SettingsTile.switchTile(
title: Text(translate('Enable hardware codec')),
initialValue: _enableHardwareCodec,
onToggle: isOptionFixed(kOptionEnableHwcodec)
? null
: (v) async {
await mainSetBoolOption(kOptionEnableHwcodec, v);
final newValue =
await mainGetBoolOption(kOptionEnableHwcodec);
setState(() {
_enableHardwareCodec = newValue;
});
},
),
]),
if (isAndroid && !outgoingOnly)
SettingsSection( SettingsSection(
title: Text(translate("Recording")), title: Text(translate("Recording")),
tiles: [ tiles: [
@@ -502,34 +604,34 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
title: title:
Text(translate('Automatically record incoming sessions')), Text(translate('Automatically record incoming sessions')),
leading: Icon(Icons.videocam), leading: Icon(Icons.videocam),
description: FutureBuilder( description: Text(
builder: (ctx, data) => Offstage( "${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
offstage: !data.hasData,
child: Text("${translate("Directory")}: ${data.data}")),
future: bind.mainDefaultVideoSaveDirectory()),
initialValue: _autoRecordIncomingSession, initialValue: _autoRecordIncomingSession,
onToggle: (v) async { onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
await bind.mainSetOption( ? null
key: "allow-auto-record-incoming", : (v) async {
value: bool2option("allow-auto-record-incoming", v)); await bind.mainSetOption(
final newValue = option2bool( key: kOptionAllowAutoRecordIncoming,
'allow-auto-record-incoming', value:
await bind.mainGetOption( bool2option(kOptionAllowAutoRecordIncoming, v));
key: 'allow-auto-record-incoming')); final newValue = option2bool(
setState(() { kOptionAllowAutoRecordIncoming,
_autoRecordIncomingSession = newValue; await bind.mainGetOption(
}); key: kOptionAllowAutoRecordIncoming));
}, setState(() {
_autoRecordIncomingSession = newValue;
});
},
), ),
], ],
), ),
if (isAndroid) if (isAndroid && !disabledSettings && !outgoingOnly)
SettingsSection( SettingsSection(
title: Text(translate("Share Screen")), title: Text(translate("Share Screen")),
tiles: shareScreenTiles, tiles: shareScreenTiles,
), ),
defaultDisplaySection(), if (!bind.isIncomingOnly()) defaultDisplaySection(),
if (isAndroid) if (isAndroid && !disabledSettings && !outgoingOnly)
SettingsSection( SettingsSection(
title: Text(translate("Enhancements")), title: Text(translate("Enhancements")),
tiles: enhancementsTiles, tiles: enhancementsTiles,
@@ -578,6 +680,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
), ),
], ],
); );
return settings;
} }
Future<bool> canStartOnBoot() async { Future<bool> canStartOnBoot() async {
@@ -617,29 +720,32 @@ void showServerSettings(OverlayDialogManager dialogManager) async {
void showLanguageSettings(OverlayDialogManager dialogManager) async { void showLanguageSettings(OverlayDialogManager dialogManager) async {
try { try {
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>; final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
var lang = bind.mainGetLocalOption(key: "lang"); var lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
dialogManager.show((setState, close, context) { dialogManager.show((setState, close, context) {
setLang(v) async { setLang(v) async {
if (lang != v) { if (lang != v) {
setState(() { setState(() {
lang = v; lang = v;
}); });
await bind.mainSetLocalOption(key: "lang", value: v); await bind.mainSetLocalOption(key: kCommConfKeyLang, value: v);
HomePage.homeKey.currentState?.refreshPages(); HomePage.homeKey.currentState?.refreshPages();
Future.delayed(Duration(milliseconds: 200), close); Future.delayed(Duration(milliseconds: 200), close);
} }
} }
final isOptFixed = isOptionFixed(kCommConfKeyLang);
return CustomAlertDialog( return CustomAlertDialog(
content: Column( content: Column(
children: [ children: [
getRadio(Text(translate('Default')), '', lang, setLang), getRadio(Text(translate('Default')), defaultOptionLang, lang,
isOptFixed ? null : setLang),
Divider(color: MyTheme.border), Divider(color: MyTheme.border),
] + ] +
langs.map((e) { langs.map((e) {
final key = e[0] as String; final key = e[0] as String;
final name = e[1] as String; final name = e[1] as String;
return getRadio(Text(translate(name)), key, lang, setLang); return getRadio(Text(translate(name)), key, lang,
isOptFixed ? null : setLang);
}).toList(), }).toList(),
), ),
); );
@@ -663,13 +769,15 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
} }
} }
final isOptFixed = isOptionFixed(kCommConfKeyTheme);
return CustomAlertDialog( return CustomAlertDialog(
content: Column(children: [ content: Column(children: [
getRadio( getRadio(Text(translate('Light')), ThemeMode.light, themeMode,
Text(translate('Light')), ThemeMode.light, themeMode, setTheme), isOptFixed ? null : setTheme),
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode, setTheme), getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode,
isOptFixed ? null : setTheme),
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode, getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
setTheme) isOptFixed ? null : setTheme)
]), ]),
); );
}, backDismiss: true, clickMaskDismiss: true); }, backDismiss: true, clickMaskDismiss: true);
@@ -757,11 +865,14 @@ class __DisplayPageState extends State<_DisplayPage> {
_RadioEntry('Scale original', kRemoteViewStyleOriginal), _RadioEntry('Scale original', kRemoteViewStyleOriginal),
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive) _RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
], ],
getter: () => bind.mainGetUserDefaultOption(key: 'view_style'), getter: () =>
asyncSetter: (value) async { bind.mainGetUserDefaultOption(key: kOptionViewStyle),
await bind.mainSetUserDefaultOption( asyncSetter: isOptionFixed(kOptionViewStyle)
key: 'view_style', value: value); ? null
}, : (value) async {
await bind.mainSetUserDefaultOption(
key: kOptionViewStyle, value: value);
},
), ),
_getPopupDialogRadioEntry( _getPopupDialogRadioEntry(
title: 'Default Image Quality', title: 'Default Image Quality',
@@ -772,16 +883,19 @@ class __DisplayPageState extends State<_DisplayPage> {
_RadioEntry('Custom', kRemoteImageQualityCustom), _RadioEntry('Custom', kRemoteImageQualityCustom),
], ],
getter: () { getter: () {
final v = bind.mainGetUserDefaultOption(key: 'image_quality'); final v =
bind.mainGetUserDefaultOption(key: kOptionImageQuality);
showCustomImageQuality.value = v == kRemoteImageQualityCustom; showCustomImageQuality.value = v == kRemoteImageQualityCustom;
return v; return v;
}, },
asyncSetter: (value) async { asyncSetter: isOptionFixed(kOptionImageQuality)
await bind.mainSetUserDefaultOption( ? null
key: 'image_quality', value: value); : (value) async {
showCustomImageQuality.value = await bind.mainSetUserDefaultOption(
value == kRemoteImageQualityCustom; key: kOptionImageQuality, value: value);
}, showCustomImageQuality.value =
value == kRemoteImageQualityCustom;
},
tail: customImageQualitySetting(), tail: customImageQualitySetting(),
showTail: showCustomImageQuality, showTail: showCustomImageQuality,
notCloseValue: kRemoteImageQualityCustom, notCloseValue: kRemoteImageQualityCustom,
@@ -790,11 +904,13 @@ class __DisplayPageState extends State<_DisplayPage> {
title: 'Default Codec', title: 'Default Codec',
list: codecList, list: codecList,
getter: () => getter: () =>
bind.mainGetUserDefaultOption(key: 'codec-preference'), bind.mainGetUserDefaultOption(key: kOptionCodecPreference),
asyncSetter: (value) async { asyncSetter: isOptionFixed(kOptionCodecPreference)
await bind.mainSetUserDefaultOption( ? null
key: 'codec-preference', value: value); : (value) async {
}, await bind.mainSetUserDefaultOption(
key: kOptionCodecPreference, value: value);
},
), ),
], ],
), ),
@@ -809,13 +925,17 @@ class __DisplayPageState extends State<_DisplayPage> {
SettingsTile otherRow(String label, String key) { SettingsTile otherRow(String label, String key) {
final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
final isOptFixed = isOptionFixed(key);
return SettingsTile.switchTile( return SettingsTile.switchTile(
initialValue: value, initialValue: value,
title: Text(translate(label)), title: Text(translate(label)),
onToggle: (b) async { onToggle: isOptFixed
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : ''); ? null
setState(() {}); : (b) async {
}, await bind.mainSetUserDefaultOption(
key: key, value: b ? 'Y' : defaultOptionNo);
setState(() {});
},
); );
} }
} }
@@ -829,11 +949,11 @@ class _RadioEntry {
typedef _RadioEntryGetter = String Function(); typedef _RadioEntryGetter = String Function();
typedef _RadioEntrySetter = Future<void> Function(String); typedef _RadioEntrySetter = Future<void> Function(String);
_getPopupDialogRadioEntry({ SettingsTile _getPopupDialogRadioEntry({
required String title, required String title,
required List<_RadioEntry> list, required List<_RadioEntry> list,
required _RadioEntryGetter getter, required _RadioEntryGetter getter,
required _RadioEntrySetter asyncSetter, required _RadioEntrySetter? asyncSetter,
Widget? tail, Widget? tail,
RxBool? showTail, RxBool? showTail,
String? notCloseValue, String? notCloseValue,
@@ -853,21 +973,23 @@ _getPopupDialogRadioEntry({
void showDialog() async { void showDialog() async {
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
onChanged(String? value) async { final onChanged = asyncSetter == null
if (value == null) return; ? null
await asyncSetter(value); : (String? value) async {
init(); if (value == null) return;
if (value != notCloseValue) { await asyncSetter(value);
close(); init();
} if (value != notCloseValue) {
} close();
}
};
return CustomAlertDialog( return CustomAlertDialog(
content: Obx( content: Obx(
() => Column(children: [ () => Column(children: [
...list ...list
.map((e) => getRadio(Text(translate(e.label)), e.value, .map((e) => getRadio(Text(translate(e.label)), e.value,
groupValue.value, (String? value) => onChanged(value))) groupValue.value, onChanged))
.toList(), .toList(),
Offstage( Offstage(
offstage: offstage:
@@ -881,7 +1003,7 @@ _getPopupDialogRadioEntry({
return SettingsTile( return SettingsTile(
title: Text(translate(title)), title: Text(translate(title)),
onPressed: (context) => showDialog(), onPressed: asyncSetter == null ? null : (context) => showDialog(),
value: Padding( value: Padding(
padding: EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.symmetric(vertical: 8),
child: Obx(() => Text(translate(valueText.value))), child: Obx(() => Text(translate(valueText.value))),

View File

@@ -283,12 +283,3 @@ void setPrivacyModeDialog(
); );
}, backDismiss: true, clickMaskDismiss: true); }, backDismiss: true, clickMaskDismiss: true);
} }
Future<String?> validateAsync(String value) async {
value = value.trim();
if (value.isEmpty) {
return null;
}
final res = await bind.mainTestIfValidServer(server: value);
return res.isEmpty ? null : res;
}

View File

@@ -5,13 +5,14 @@ import 'dart:io';
import 'package:flutter/material.dart'; 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/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:http/http.dart' as http;
import '../utils/http_service.dart' as http;
import '../common.dart'; import '../common.dart';
final syncAbOption = 'sync-ab-with-recent-sessions'; final syncAbOption = 'sync-ab-with-recent-sessions';
@@ -84,7 +85,7 @@ class AbModel {
reset() async { reset() async {
print("reset ab model"); print("reset ab model");
addressbooks.clear(); addressbooks.clear();
setCurrentName(''); _currentName.value = '';
await bind.mainClearAb(); await bind.mainClearAb();
listInitialized = false; listInitialized = false;
} }
@@ -509,7 +510,8 @@ class AbModel {
} }
void setShouldAsync(bool v) async { void setShouldAsync(bool v) async {
await bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : ''); await bind.mainSetLocalOption(
key: syncAbOption, value: v ? 'Y' : defaultOptionNo);
_syncAllFromRecent = true; _syncAllFromRecent = true;
_timerCounter = 0; _timerCounter = 0;
} }
@@ -548,7 +550,7 @@ class AbModel {
} }
trySetCurrentToLast() { trySetCurrentToLast() {
final name = bind.getLocalFlutterOption(k: 'current-ab-name'); final name = bind.getLocalFlutterOption(k: kOptionCurrentAbName);
if (addressbooks.containsKey(name)) { if (addressbooks.containsKey(name)) {
_currentName.value = name; _currentName.value = name;
} }
@@ -647,6 +649,10 @@ class AbModel {
return addressbooks.keys.toList(); return addressbooks.keys.toList();
} }
String personalAddressBookName() {
return _personalAddressBookName;
}
Future<void> setCurrentName(String name) async { Future<void> setCurrentName(String name) async {
final oldName = _currentName.value; final oldName = _currentName.value;
if (addressbooks.containsKey(name)) { if (addressbooks.containsKey(name)) {

View File

@@ -527,10 +527,18 @@ class ChatModel with ChangeNotifier {
void onVoiceCallStarted() { void onVoiceCallStarted() {
_voiceCallStatus.value = VoiceCallStatus.connected; _voiceCallStatus.value = VoiceCallStatus.connected;
if (isAndroid) {
parent.target?.invokeMethod("on_voice_call_started");
}
} }
void onVoiceCallClosed(String reason) { void onVoiceCallClosed(String reason) {
_voiceCallStatus.value = VoiceCallStatus.notStarted; _voiceCallStatus.value = VoiceCallStatus.notStarted;
if (isAndroid) {
// We can always invoke "on_voice_call_closed"
// no matter if the `_voiceCallStatus` was `VoiceCallStatus.notStarted` or not.
parent.target?.invokeMethod("on_voice_call_closed");
}
} }
void onVoiceCallIncoming() { void onVoiceCallIncoming() {

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart'; import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -10,15 +11,10 @@ import './platform_model.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart' import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart'; if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
// Feature flutter_texture_render need to be enabled if feature gpucodec is enabled.
final useTextureRender = !isWeb &&
(bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender());
class _PixelbufferTexture { class _PixelbufferTexture {
int _textureKey = -1; int _textureKey = -1;
int _display = 0; int _display = 0;
SessionID? _sessionId; SessionID? _sessionId;
final support = bind.mainHasPixelbufferTextureRender();
bool _destroying = false; bool _destroying = false;
int? _id; int? _id;
@@ -27,26 +23,24 @@ class _PixelbufferTexture {
int get display => _display; int get display => _display;
create(int d, SessionID sessionId, FFI ffi) { create(int d, SessionID sessionId, FFI ffi) {
if (support) { _display = d;
_display = d; _textureKey = bind.getNextTextureKey();
_textureKey = bind.getNextTextureKey(); _sessionId = sessionId;
_sessionId = sessionId;
textureRenderer.createTexture(_textureKey).then((id) async { textureRenderer.createTexture(_textureKey).then((id) async {
_id = id; _id = id;
if (id != -1) { if (id != -1) {
ffi.textureModel.setRgbaTextureId(display: d, id: id); ffi.textureModel.setRgbaTextureId(display: d, id: id);
final ptr = await textureRenderer.getTexturePtr(_textureKey); final ptr = await textureRenderer.getTexturePtr(_textureKey);
platformFFI.registerPixelbufferTexture(sessionId, display, ptr); platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
debugPrint( debugPrint(
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id"); "create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
} }
}); });
}
} }
destroy(bool unregisterTexture, FFI ffi) async { destroy(bool unregisterTexture, FFI ffi) async {
if (!_destroying && support && _textureKey != -1 && _sessionId != null) { if (!_destroying && _textureKey != -1 && _sessionId != null) {
_destroying = true; _destroying = true;
if (unregisterTexture) { if (unregisterTexture) {
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0); platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
@@ -101,13 +95,15 @@ class _GpuTexture {
} }
} }
destroy(FFI ffi) async { destroy(bool unregisterTexture, FFI ffi) async {
// must stop texture render, render unregistered texture cause crash // must stop texture render, render unregistered texture cause crash
if (!_destroying && support && _sessionId != null && _textureId != -1) { if (!_destroying && support && _sessionId != null && _textureId != -1) {
_destroying = true; _destroying = true;
platformFFI.registerGpuTexture(_sessionId!, _display, 0); if (unregisterTexture) {
// sleep for a while to avoid the texture is used after it's unregistered. platformFFI.registerGpuTexture(_sessionId!, _display, 0);
await Future.delayed(Duration(milliseconds: 100)); // sleep for a while to avoid the texture is used after it's unregistered.
await Future.delayed(Duration(milliseconds: 100));
}
await gpuTextureRenderer.unregisterTexture(_textureId); await gpuTextureRenderer.unregisterTexture(_textureId);
_textureId = -1; _textureId = -1;
_destroying = false; _destroying = false;
@@ -152,40 +148,36 @@ class TextureModel {
TextureModel(this.parent); TextureModel(this.parent);
setTextureType({required int display, required bool gpuTexture}) { setTextureType({required int display, required bool gpuTexture}) {
debugPrint("setTextureType: display:$display, isGpuTexture:$gpuTexture"); debugPrint("setTextureType: display=$display, isGpuTexture=$gpuTexture");
var texture = _control[display]; ensureControl(display);
if (texture == null) { _control[display]?.setTextureType(gpuTexture: gpuTexture);
texture = _Control(); // For versions that do not support multiple displays, the display parameter is always 0, need set type of current display
_control[display] = texture; final ffi = parent.target;
if (ffi == null) return;
if (!ffi.ffiModel.pi.isSupportMultiDisplay) {
final currentDisplay = CurrentDisplayState.find(ffi.id).value;
if (currentDisplay != display) {
debugPrint(
"setTextureType: currentDisplay=$currentDisplay, isGpuTexture=$gpuTexture");
ensureControl(currentDisplay);
_control[currentDisplay]?.setTextureType(gpuTexture: gpuTexture);
}
} }
texture.setTextureType(gpuTexture: gpuTexture);
} }
setRgbaTextureId({required int display, required int id}) { setRgbaTextureId({required int display, required int id}) {
var ctl = _control[display]; ensureControl(display);
if (ctl == null) { _control[display]?.setRgbaTextureId(id);
ctl = _Control();
_control[display] = ctl;
}
ctl.setRgbaTextureId(id);
} }
setGpuTextureId({required int display, required int id}) { setGpuTextureId({required int display, required int id}) {
var ctl = _control[display]; ensureControl(display);
if (ctl == null) { _control[display]?.setGpuTextureId(id);
ctl = _Control();
_control[display] = ctl;
}
ctl.setGpuTextureId(id);
} }
RxInt getTextureId(int display) { RxInt getTextureId(int display) {
var ctl = _control[display]; ensureControl(display);
if (ctl == null) { return _control[display]!.textureID;
ctl = _Control();
_control[display] = ctl;
}
return ctl.textureID;
} }
updateCurrentDisplay(int curDisplay) { updateCurrentDisplay(int curDisplay) {
@@ -211,7 +203,7 @@ class TextureModel {
_pixelbufferRenderTextures.remove(idx); _pixelbufferRenderTextures.remove(idx);
} }
if (_gpuRenderTextures.containsKey(idx)) { if (_gpuRenderTextures.containsKey(idx)) {
_gpuRenderTextures[idx]!.destroy(ffi); _gpuRenderTextures[idx]!.destroy(true, ffi);
_gpuRenderTextures.remove(idx); _gpuRenderTextures.remove(idx);
} }
} }
@@ -238,7 +230,15 @@ class TextureModel {
await texture.destroy(closeSession, ffi); await texture.destroy(closeSession, ffi);
} }
for (final texture in _gpuRenderTextures.values) { for (final texture in _gpuRenderTextures.values) {
await texture.destroy(ffi); await texture.destroy(closeSession, ffi);
}
}
ensureControl(int display) {
var ctl = _control[display];
if (ctl == null) {
ctl = _Control();
_control[display] = ctl;
} }
} }
} }

View File

@@ -7,7 +7,7 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import '../utils/http_service.dart' as http;
class GroupModel { class GroupModel {
final RxBool groupLoading = false.obs; final RxBool groupLoading = false.obs;
@@ -26,6 +26,7 @@ class GroupModel {
GroupModel(this.parent); GroupModel(this.parent);
Future<void> pull({force = true, quiet = false}) async { Future<void> pull({force = true, quiet = false}) async {
if (bind.isDisableGroupPanel()) return;
if (!gFFI.userModel.isLogin || groupLoading.value) return; if (!gFFI.userModel.isLogin || groupLoading.value) return;
if (!force && initialized) return; if (!force && initialized) return;
if (!quiet) { if (!quiet) {

View File

@@ -4,9 +4,12 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../models/model.dart'; import '../../models/model.dart';
@@ -21,6 +24,128 @@ const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup'; const _kMouseEventUp = 'mouseup';
const _kMouseEventMove = 'mousemove'; const _kMouseEventMove = 'mousemove';
class CanvasCoords {
double x = 0;
double y = 0;
double scale = 1.0;
double scrollX = 0;
double scrollY = 0;
ScrollStyle scrollStyle = ScrollStyle.scrollauto;
Size size = Size.zero;
CanvasCoords();
Map<String, dynamic> toJson() {
return {
'x': x,
'y': y,
'scale': scale,
'scrollX': scrollX,
'scrollY': scrollY,
'scrollStyle':
scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar',
'size': {
'w': size.width,
'h': size.height,
}
};
}
static CanvasCoords fromJson(Map<String, dynamic> json) {
final model = CanvasCoords();
model.x = json['x'];
model.y = json['y'];
model.scale = json['scale'];
model.scrollX = json['scrollX'];
model.scrollY = json['scrollY'];
model.scrollStyle = json['scrollStyle'] == 'scrollauto'
? ScrollStyle.scrollauto
: ScrollStyle.scrollbar;
model.size = Size(json['size']['w'], json['size']['h']);
return model;
}
static CanvasCoords fromCanvasModel(CanvasModel model) {
final coords = CanvasCoords();
coords.x = model.x;
coords.y = model.y;
coords.scale = model.scale;
coords.scrollX = model.scrollX;
coords.scrollY = model.scrollY;
coords.scrollStyle = model.scrollStyle;
coords.size = model.size;
return coords;
}
}
class CursorCoords {
Offset offset = Offset.zero;
CursorCoords();
Map<String, dynamic> toJson() {
return {
'offset_x': offset.dx,
'offset_y': offset.dy,
};
}
static CursorCoords fromJson(Map<String, dynamic> json) {
final model = CursorCoords();
model.offset = Offset(json['offset_x'], json['offset_y']);
return model;
}
static CursorCoords fromCursorModel(CursorModel model) {
final coords = CursorCoords();
coords.offset = model.offset;
return coords;
}
}
class RemoteWindowCoords {
RemoteWindowCoords(
this.windowRect, this.canvas, this.cursor, this.remoteRect);
Rect windowRect;
CanvasCoords canvas;
CursorCoords cursor;
Rect remoteRect;
Offset relativeOffset = Offset.zero;
Map<String, dynamic> toJson() {
return {
'canvas': canvas.toJson(),
'cursor': cursor.toJson(),
'windowRect': rectToJson(windowRect),
'remoteRect': rectToJson(remoteRect),
};
}
static Map<String, dynamic> rectToJson(Rect r) {
return {
'l': r.left,
't': r.top,
'w': r.width,
'h': r.height,
};
}
static Rect rectFromJson(Map<String, dynamic> json) {
return Rect.fromLTWH(
json['l'],
json['t'],
json['w'],
json['h'],
);
}
RemoteWindowCoords.fromJson(Map<String, dynamic> json)
: windowRect = rectFromJson(json['windowRect']),
canvas = CanvasCoords.fromJson(json['canvas']),
cursor = CursorCoords.fromJson(json['cursor']),
remoteRect = rectFromJson(json['remoteRect']);
}
extension ToString on MouseButtons { extension ToString on MouseButtons {
String get value { String get value {
switch (this) { switch (this) {
@@ -188,32 +313,29 @@ class InputModel {
int _lastButtons = 0; int _lastButtons = 0;
Offset lastMousePos = Offset.zero; Offset lastMousePos = Offset.zero;
bool _queryOtherWindowCoords = false;
Rect? _windowRect;
List<RemoteWindowCoords> _remoteWindowCoords = [];
late final SessionID sessionId; late final SessionID sessionId;
bool get keyboardPerm => parent.target!.ffiModel.keyboard; bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? ''; String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform; String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly; bool get isViewOnly => parent.target!.ffiModel.viewOnly;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
InputModel(this.parent) { InputModel(this.parent) {
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
// It is ok to call updateKeyboardMode() directly.
// Because `bind` is initialized in `PlatformFFI.init()` which is called very early.
// But we still wrap it in a Future.delayed() to make it more clear.
Future.delayed(Duration(milliseconds: 100), () {
updateKeyboardMode();
});
} }
// This function must be called after the peer info is received.
// Because `sessionGetKeyboardMode` relies on the peer version.
updateKeyboardMode() async { updateKeyboardMode() async {
// * Currently mobile does not enable map mode // * Currently mobile does not enable map mode
if (isDesktop || isWebDesktop) { if (isDesktop || isWebDesktop) {
if (keyboardMode.isEmpty) { keyboardMode = await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
keyboardMode = kKeyLegacyMode;
await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
kKeyLegacyMode;
}
} }
} }
@@ -616,6 +738,9 @@ class InputModel {
void onPointDownImage(PointerDownEvent e) { void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}"); debugPrint("onPointDownImage ${e.kind}");
_stopFling = true; _stopFling = true;
if (isDesktop) _queryOtherWindowCoords = true;
_remoteWindowCoords = [];
_windowRect = null;
if (isViewOnly) return; if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) { if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
@@ -628,6 +753,7 @@ class InputModel {
} }
void onPointUpImage(PointerUpEvent e) { void onPointUpImage(PointerUpEvent e) {
if (isDesktop) _queryOtherWindowCoords = false;
if (isViewOnly) return; if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
@@ -638,11 +764,37 @@ class InputModel {
void onPointMoveImage(PointerMoveEvent e) { void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return; if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_queryOtherWindowCoords) {
Future.delayed(Duration.zero, () async {
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
});
_queryOtherWindowCoords = false;
}
if (isPhysicalMouse.value) { if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position); handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
} }
} }
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
List<RemoteWindowCoords> remoteWindowCoords) async {
final coords =
await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain();
final wc = WindowController.fromWindowId(kWindowId!);
try {
final frame = await wc.getFrame();
for (final c in coords) {
c.relativeOffset = Offset(
c.windowRect.left - frame.left, c.windowRect.top - frame.top);
remoteWindowCoords.add(c);
}
return frame;
} catch (e) {
// Unreachable code
debugPrint("Failed to get frame of window $kWindowId, it may be hidden");
}
return null;
}
void onPointerSignalImage(PointerSignalEvent e) { void onPointerSignalImage(PointerSignalEvent e) {
if (isViewOnly) return; if (isViewOnly) return;
if (e is PointerScrollEvent) { if (e is PointerScrollEvent) {
@@ -843,43 +995,107 @@ class InputModel {
bool onExit = false, bool onExit = false,
int buttons = kPrimaryMouseButton, int buttons = kPrimaryMouseButton,
}) { }) {
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
final canvasModel = parent.target!.canvasModel;
final ffiModel = parent.target!.ffiModel; final ffiModel = parent.target!.ffiModel;
CanvasCoords canvas =
CanvasCoords.fromCanvasModel(parent.target!.canvasModel);
Rect? rect = ffiModel.rect;
if (isMove) { if (isMove) {
canvasModel.moveDesktopMouse(x, y); if (_remoteWindowCoords.isNotEmpty &&
_windowRect != null &&
!_isInCurrentWindow(x, y)) {
final coords =
findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio);
if (coords != null) {
isMove = false;
canvas = coords.canvas;
rect = coords.remoteRect;
x -= coords.relativeOffset.dx / devicePixelRatio;
y -= coords.relativeOffset.dy / devicePixelRatio;
}
}
} }
final nearThr = 3; y -= CanvasModel.topToEdge;
var nearRight = (canvasModel.size.width - x) < nearThr; x -= CanvasModel.leftToEdge;
var nearBottom = (canvasModel.size.height - y) < nearThr; if (isMove) {
final rect = ffiModel.rect; parent.target!.canvasModel.moveDesktopMouse(x, y);
}
return _handlePointerDevicePos(
kind,
x,
y,
isMove,
canvas,
rect,
evtType,
onExit: onExit,
buttons: buttons,
);
}
bool _isInCurrentWindow(double x, double y) {
final w = _windowRect!.width / devicePixelRatio;
final h = _windowRect!.width / devicePixelRatio;
return x >= 0 && y >= 0 && x <= w && y <= h;
}
static RemoteWindowCoords? findRemoteCoords(double x, double y,
List<RemoteWindowCoords> remoteWindowCoords, double devicePixelRatio) {
x *= devicePixelRatio;
y *= devicePixelRatio;
for (final c in remoteWindowCoords) {
if (x >= c.relativeOffset.dx &&
y >= c.relativeOffset.dy &&
x <= c.relativeOffset.dx + c.windowRect.width &&
y <= c.relativeOffset.dy + c.windowRect.height) {
return c;
}
}
return null;
}
Point? _handlePointerDevicePos(
String kind,
double x,
double y,
bool moveInCanvas,
CanvasCoords canvas,
Rect? rect,
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
}) {
if (rect == null) { if (rect == null) {
return null; return null;
} }
final imageWidth = rect.width * canvasModel.scale;
final imageHeight = rect.height * canvasModel.scale; final nearThr = 3;
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { var nearRight = (canvas.size.width - x) < nearThr;
x += imageWidth * canvasModel.scrollX; var nearBottom = (canvas.size.height - y) < nearThr;
y += imageHeight * canvasModel.scrollY; final imageWidth = rect.width * canvas.scale;
final imageHeight = rect.height * canvas.scale;
if (canvas.scrollStyle == ScrollStyle.scrollbar) {
x += imageWidth * canvas.scrollX;
y += imageHeight * canvas.scrollY;
// boxed size is a center widget // boxed size is a center widget
if (canvasModel.size.width > imageWidth) { if (canvas.size.width > imageWidth) {
x -= ((canvasModel.size.width - imageWidth) / 2); x -= ((canvas.size.width - imageWidth) / 2);
} }
if (canvasModel.size.height > imageHeight) { if (canvas.size.height > imageHeight) {
y -= ((canvasModel.size.height - imageHeight) / 2); y -= ((canvas.size.height - imageHeight) / 2);
} }
} else { } else {
x -= canvasModel.x; x -= canvas.x;
y -= canvasModel.y; y -= canvas.y;
} }
x /= canvasModel.scale; x /= canvas.scale;
y /= canvasModel.scale; y /= canvas.scale;
if (canvasModel.scale > 0 && canvasModel.scale < 1) { if (canvas.scale > 0 && canvas.scale < 1) {
final step = 1.0 / canvasModel.scale - 1; final step = 1.0 / canvas.scale - 1;
if (nearRight) { if (nearRight) {
x += step; x += step;
} }
@@ -902,8 +1118,7 @@ class InputModel {
evtX = x.round(); evtX = x.round();
evtY = y.round(); evtY = y.round();
} catch (e) { } catch (e) {
debugPrintStack( debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e');
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
return null; return null;
} }

View File

@@ -50,6 +50,8 @@ class CachedPeerData {
Map<String, dynamic> peerInfo = {}; Map<String, dynamic> peerInfo = {};
List<Map<String, dynamic>> cursorDataList = []; List<Map<String, dynamic>> cursorDataList = [];
Map<String, dynamic> lastCursorId = {}; Map<String, dynamic> lastCursorId = {};
Map<String, bool> permissions = {};
bool secure = false; bool secure = false;
bool direct = false; bool direct = false;
@@ -62,6 +64,7 @@ class CachedPeerData {
'peerInfo': peerInfo, 'peerInfo': peerInfo,
'cursorDataList': cursorDataList, 'cursorDataList': cursorDataList,
'lastCursorId': lastCursorId, 'lastCursorId': lastCursorId,
'permissions': permissions,
'secure': secure, 'secure': secure,
'direct': direct, 'direct': direct,
}); });
@@ -77,6 +80,9 @@ class CachedPeerData {
data.cursorDataList.add(cursorData); data.cursorDataList.add(cursorData);
} }
data.lastCursorId = map['lastCursorId']; data.lastCursorId = map['lastCursorId'];
map['permissions'].forEach((key, value) {
data.permissions[key] = value;
});
data.secure = map['secure']; data.secure = map['secure'];
data.direct = map['direct']; data.direct = map['direct'];
return data; return data;
@@ -106,6 +112,7 @@ class FfiModel with ChangeNotifier {
RxBool waitForImageDialogShow = true.obs; RxBool waitForImageDialogShow = true.obs;
Timer? waitForImageTimer; Timer? waitForImageTimer;
RxBool waitForFirstImage = true.obs; RxBool waitForFirstImage = true.obs;
bool isRefreshing = false;
Rect? get rect => _rect; Rect? get rect => _rect;
bool get isOriginalResolutionSet => bool get isOriginalResolutionSet =>
@@ -116,6 +123,10 @@ class FfiModel with ChangeNotifier {
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false; _pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false;
Map<String, bool> get permissions => _permissions; Map<String, bool> get permissions => _permissions;
setPermissions(Map<String, bool> permissions) {
_permissions.clear();
_permissions.addAll(permissions);
}
bool? get secure => _secure; bool? get secure => _secure;
@@ -138,6 +149,7 @@ class FfiModel with ChangeNotifier {
FfiModel(this.parent) { FfiModel(this.parent) {
clear(); clear();
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
cachedPeerData.permissions = _permissions;
} }
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true); Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
@@ -233,7 +245,7 @@ class FfiModel with ChangeNotifier {
handleMsgBox({ handleMsgBox({
'type': 'success', 'type': 'success',
'title': 'Successful', 'title': 'Successful',
'text': 'Connected, waiting for image...', 'text': kMsgboxTextWaitingForImage,
'link': '', 'link': '',
}, sessionId, peerId); }, sessionId, peerId);
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId); updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
@@ -367,16 +379,29 @@ class FfiModel with ChangeNotifier {
} }
} else if (name == 'sync_peer_option') { } else if (name == 'sync_peer_option') {
_handleSyncPeerOption(evt, peerId); _handleSyncPeerOption(evt, peerId);
} else if (name == 'follow_current_display') {
handleFollowCurrentDisplay(evt, sessionId, peerId);
} else if (name == 'use_texture_render') {
_handleUseTextureRender(evt, sessionId, peerId);
} else { } else {
debugPrint('Unknown event name: $name'); debugPrint('Unknown event name: $name');
} }
}; };
} }
_handleUseTextureRender(
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
waitForFirstImage.value = true;
isRefreshing = true;
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
'success', 'Successful', kMsgboxTextWaitingForImage);
}
_handleSyncPeerOption(Map<String, dynamic> evt, String peer) { _handleSyncPeerOption(Map<String, dynamic> evt, String peer) {
final k = evt['k']; final k = evt['k'];
final v = evt['v']; final v = evt['v'];
if (k == kOptionViewOnly) { if (k == kOptionToggleViewOnly) {
setViewOnly(peer, v as bool); setViewOnly(peer, v as bool);
} else if (k == 'keyboard_mode') { } else if (k == 'keyboard_mode') {
parent.target?.inputModel.updateKeyboardMode(); parent.target?.inputModel.updateKeyboardMode();
@@ -440,7 +465,7 @@ class FfiModel with ChangeNotifier {
} }
} }
updateCurDisplay(SessionID sessionId, {updateCursorPos = true}) { updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) {
final newRect = displaysRect(); final newRect = displaysRect();
if (newRect == null) { if (newRect == null) {
return; return;
@@ -559,10 +584,14 @@ class FfiModel with ChangeNotifier {
showElevationError(sessionId, type, title, text, dialogManager); showElevationError(sessionId, type, title, text, dialogManager);
} else if (type == 'relay-hint' || type == 'relay-hint2') { } else if (type == 'relay-hint' || type == 'relay-hint2') {
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId); showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
} else if (text == 'Connected, waiting for image...') { } else if (text == kMsgboxTextWaitingForImage) {
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text); showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
} else if (title == 'Privacy mode') {
final hasRetry = evt['hasRetry'] == 'true';
showPrivacyFailedDialog(
sessionId, type, title, text, link, hasRetry, dialogManager);
} else { } else {
var hasRetry = evt['hasRetry'] == 'true'; final hasRetry = evt['hasRetry'] == 'true';
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
} }
} }
@@ -650,13 +679,34 @@ class FfiModel with ChangeNotifier {
); );
waitForImageDialogShow.value = true; waitForImageDialogShow.value = true;
waitForImageTimer = Timer(Duration(milliseconds: 1500), () { waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
if (waitForFirstImage.isTrue) { if (waitForFirstImage.isTrue && !isRefreshing) {
bind.sessionInputOsPassword(sessionId: sessionId, value: ''); bind.sessionInputOsPassword(sessionId: sessionId, value: '');
} }
}); });
bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId); bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId);
} }
void showPrivacyFailedDialog(
SessionID sessionId,
String type,
String title,
String text,
String link,
bool hasRetry,
OverlayDialogManager dialogManager) {
if (text == 'no_need_privacy_mode_no_physical_displays_tip' ||
text == 'Enter privacy mode') {
// There are display changes on the remote side,
// which will cause some messages to refresh the canvas and dismiss dialogs.
// So we add a delay here to ensure the dialog is displayed.
Future.delayed(Duration(milliseconds: 3000), () {
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
});
} else {
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
}
}
_updateSessionWidthHeight(SessionID sessionId) { _updateSessionWidthHeight(SessionID sessionId) {
if (_rect == null) return; if (_rect == null) return;
if (_rect!.width <= 0 || _rect!.height <= 0) { if (_rect!.width <= 0 || _rect!.height <= 0) {
@@ -687,9 +737,14 @@ 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 {
// This call is to ensuer the keyboard mode is updated depending on the peer version.
parent.target?.inputModel.updateKeyboardMode();
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference. // Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
// Because this function is asynchronous, there's an "await" in this function. // Because this function is asynchronous, there's an "await" in this function.
cachedPeerData.peerInfo = {...evt}; cachedPeerData.peerInfo = {...evt};
// Do not cache resolutions, because a new display connection have different resolutions.
cachedPeerData.peerInfo.remove('resolutions');
// Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) // Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers(); bind.mainLoadRecentPeers();
@@ -722,7 +777,7 @@ class FfiModel with ChangeNotifier {
_touchMode = true; _touchMode = true;
} else { } else {
_touchMode = await bind.sessionGetOption( _touchMode = await bind.sessionGetOption(
sessionId: sessionId, arg: 'touch-mode') != sessionId: sessionId, arg: kOptionTouchMode) !=
''; '';
} }
if (connType == ConnType.fileTransfer) { if (connType == ConnType.fileTransfer) {
@@ -742,17 +797,20 @@ class FfiModel with ChangeNotifier {
if (displays.isNotEmpty) { if (displays.isNotEmpty) {
_reconnects = 1; _reconnects = 1;
waitForFirstImage.value = true; waitForFirstImage.value = true;
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'] == 1;
handleResolutions(peerId, evt["resolutions"]); if (!isCache) {
handleResolutions(peerId, evt["resolutions"]);
}
parent.target?.elevationModel.onPeerInfo(_pi); parent.target?.elevationModel.onPeerInfo(_pi);
} }
if (connType == ConnType.defaultConn) { if (connType == ConnType.defaultConn) {
setViewOnly( setViewOnly(
peerId, peerId,
bind.sessionGetToggleOptionSync( bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: kOptionViewOnly)); sessionId: sessionId, arg: kOptionToggleViewOnly));
} }
if (connType == ConnType.defaultConn) { if (connType == ConnType.defaultConn) {
final platformAdditions = evt['platform_additions']; final platformAdditions = evt['platform_additions'];
@@ -859,10 +917,12 @@ class FfiModel with ChangeNotifier {
if (parent.target?.connType == ConnType.defaultConn && if (parent.target?.connType == ConnType.defaultConn &&
parent.target != null && parent.target != null &&
parent.target!.ffiModel.permissions['keyboard'] != false) { parent.target!.ffiModel.permissions['keyboard'] != false) {
Timer( Timer(Duration(milliseconds: delayMSecs), () {
Duration(milliseconds: delayMSecs), if (parent.target!.dialogManager.mobileActionsOverlayVisible.isTrue) {
() => parent.target!.dialogManager parent.target!.dialogManager
.showMobileActionsOverlay(ffi: parent.target!)); .showMobileActionsOverlay(ffi: parent.target!);
}
});
} }
} }
} }
@@ -915,7 +975,9 @@ class FfiModel with ChangeNotifier {
} }
updateLastCursorId(Map<String, dynamic> evt) { updateLastCursorId(Map<String, dynamic> evt) {
parent.target?.cursorModel.id = int.parse(evt['id']); // int.parse(evt['id']) may cause FormatException
// Unhandled Exception: FormatException: Positive input exceeds the limit of integer 18446744071749110741
parent.target?.cursorModel.id = evt['id'];
} }
handleCursorId(Map<String, dynamic> evt) { handleCursorId(Map<String, dynamic> evt) {
@@ -975,6 +1037,8 @@ class FfiModel with ChangeNotifier {
} }
} }
} }
parent.target!.canvasModel
.tryUpdateScrollStyle(Duration(milliseconds: 300), null);
notifyListeners(); notifyListeners();
} }
@@ -986,15 +1050,21 @@ class FfiModel with ChangeNotifier {
} }
if (updateData.isEmpty) { if (updateData.isEmpty) {
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays); _pi.platformAdditions.remove(kPlatformAdditionsRustDeskVirtualDisplays);
_pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays);
} else { } else {
try { try {
final updateJson = json.decode(updateData) as Map<String, dynamic>; final updateJson = json.decode(updateData) as Map<String, dynamic>;
for (final key in updateJson.keys) { for (final key in updateJson.keys) {
_pi.platformAdditions[key] = updateJson[key]; _pi.platformAdditions[key] = updateJson[key];
} }
if (!updateJson.containsKey(kPlatformAdditionsVirtualDisplays)) { if (!updateJson
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays); .containsKey(kPlatformAdditionsRustDeskVirtualDisplays)) {
_pi.platformAdditions
.remove(kPlatformAdditionsRustDeskVirtualDisplays);
}
if (!updateJson.containsKey(kPlatformAdditionsAmyuniVirtualDisplays)) {
_pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays);
} }
} catch (e) { } catch (e) {
debugPrint('Failed to decode platformAdditions $e'); debugPrint('Failed to decode platformAdditions $e');
@@ -1005,9 +1075,30 @@ class FfiModel with ChangeNotifier {
json.encode(_pi.platformAdditions); json.encode(_pi.platformAdditions);
} }
handleFollowCurrentDisplay(
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
if (evt['display_idx'] != null) {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
_pi.currentDisplay = int.parse(evt['display_idx']);
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([_pi.currentDisplay]),
);
}
notifyListeners();
}
// Directly switch to the new display without waiting for the response. // Directly switch to the new display without waiting for the response.
switchToNewDisplay(int display, SessionID sessionId, String peerId, switchToNewDisplay(int display, SessionID sessionId, String peerId,
{bool updateCursorPos = true}) { {bool updateCursorPos = false}) {
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays. // VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
parent.target?.recordingModel.onClose(); parent.target?.recordingModel.onClose();
// no need to wait for the response // no need to wait for the response
@@ -1082,6 +1173,8 @@ class ImageModel with ChangeNotifier {
late final SessionID sessionId; late final SessionID sessionId;
bool _useTextureRender = false;
WeakReference<FFI> parent; WeakReference<FFI> parent;
final List<Function(String)> callbacksOnFirstImage = []; final List<Function(String)> callbacksOnFirstImage = [];
@@ -1090,14 +1183,17 @@ class ImageModel with ChangeNotifier {
sessionId = parent.target!.sessionId; sessionId = parent.target!.sessionId;
} }
get useTextureRender => _useTextureRender;
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
onRgba(int display, Uint8List rgba) { onRgba(int display, Uint8List rgba) {
final pid = parent.target?.id; final pid = parent.target?.id;
final rect = parent.target?.ffiModel.pi.getDisplayRect(display);
img.decodeImageFromPixels( img.decodeImageFromPixels(
rgba, rgba,
parent.target?.ffiModel.rect?.width.toInt() ?? 0, rect?.width.toInt() ?? 0,
parent.target?.ffiModel.rect?.height.toInt() ?? 0, rect?.height.toInt() ?? 0,
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888,
onPixelsCopied: () { onPixelsCopied: () {
// Unlock the rgba memory from rust codes. // Unlock the rgba memory from rust codes.
@@ -1129,11 +1225,8 @@ class ImageModel with ChangeNotifier {
if (parent.target != null) { if (parent.target != null) {
await initializeCursorAndCanvas(parent.target!); await initializeCursorAndCanvas(parent.target!);
} }
if (parent.target?.ffiModel.isPeerAndroid ?? false) {
bind.sessionSetViewStyle(sessionId: sessionId, value: 'adaptive');
parent.target?.canvasModel.updateViewStyle();
}
} }
_image?.dispose();
_image = image; _image = image;
if (image != null) notifyListeners(); if (image != null) notifyListeners();
} }
@@ -1157,6 +1250,24 @@ class ImageModel with ChangeNotifier {
final yscale = size.height / _image!.height; final yscale = size.height / _image!.height;
return min(xscale, yscale) / 1.5; return min(xscale, yscale) / 1.5;
} }
updateUserTextureRender() {
final preValue = _useTextureRender;
_useTextureRender = isDesktop && bind.mainGetUseTextureRender();
if (preValue != _useTextureRender) {
notifyListeners();
}
}
setUseTextureRender(bool value) {
_useTextureRender = value;
notifyListeners();
}
void disposeImage() {
_image?.dispose();
_image = null;
}
} }
enum ScrollStyle { enum ScrollStyle {
@@ -1343,10 +1454,20 @@ class CanvasModel with ChangeNotifier {
if (refreshMousePos) { if (refreshMousePos) {
parent.target?.inputModel.refreshMousePos(); parent.target?.inputModel.refreshMousePos();
} }
if (style == kRemoteViewStyleOriginal && tryUpdateScrollStyle(Duration.zero, style);
_scrollStyle == ScrollStyle.scrollbar) { }
updateScrollPercent();
tryUpdateScrollStyle(Duration duration, String? style) async {
if (_scrollStyle != ScrollStyle.scrollbar) return;
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
if (style != kRemoteViewStyleOriginal) {
return;
} }
_resetScroll();
Future.delayed(duration, () async {
updateScrollPercent();
});
} }
updateScrollStyle() async { updateScrollStyle() async {
@@ -1457,22 +1578,24 @@ class CanvasModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
updateScale(double v) { updateScale(double v, Offset focalPoint) {
if (parent.target?.imageModel.image == null) return; if (parent.target?.imageModel.image == null) return;
final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0); final s = _scale;
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
final px0 = (offset.dx - r.left) * _scale;
final py0 = (offset.dy - r.top) * _scale;
_scale *= v; _scale *= v;
final maxs = parent.target?.imageModel.maxScale ?? 1; final maxs = parent.target?.imageModel.maxScale ?? 1;
final mins = parent.target?.imageModel.minScale ?? 1; final mins = parent.target?.imageModel.minScale ?? 1;
if (_scale > maxs) _scale = maxs; if (_scale > maxs) _scale = maxs;
if (_scale < mins) _scale = mins; if (_scale < mins) _scale = mins;
r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; // (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX
final px1 = (offset.dx - r.left) * _scale; // _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2
final py1 = (offset.dy - r.top) * _scale; _x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale;
_x -= px1 - px0; final adjustForKeyboard =
_y -= py1 - py0; parent.target?.cursorModel.adjustForKeyboard() ?? 0.0;
// (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY
// _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2
_y = focalPoint.dy +
adjustForKeyboard -
(focalPoint.dy - _y + adjustForKeyboard) / s * _scale;
notifyListeners(); notifyListeners();
} }
@@ -1503,7 +1626,7 @@ class CanvasModel with ChangeNotifier {
// data for cursor // data for cursor
class CursorData { class CursorData {
final String peerId; final String peerId;
final int id; final String id;
final img2.Image image; final img2.Image image;
double scale; double scale;
Uint8List? data; Uint8List? data;
@@ -1583,13 +1706,15 @@ const _forbiddenCursorPng =
const _defaultCursorPng = const _defaultCursorPng =
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC'; 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
const kPreForbiddenCursorId = "-2";
final preForbiddenCursor = PredefinedCursor( final preForbiddenCursor = PredefinedCursor(
png: _forbiddenCursorPng, png: _forbiddenCursorPng,
id: -2, id: kPreForbiddenCursorId,
); );
const kPreDefaultCursorId = "-1";
final preDefaultCursor = PredefinedCursor( final preDefaultCursor = PredefinedCursor(
png: _defaultCursorPng, png: _defaultCursorPng,
id: -1, id: kPreDefaultCursorId,
hotxGetter: (double w) => w / 2, hotxGetter: (double w) => w / 2,
hotyGetter: (double h) => h / 2, hotyGetter: (double h) => h / 2,
); );
@@ -1599,7 +1724,7 @@ class PredefinedCursor {
img2.Image? _image2; img2.Image? _image2;
CursorData? _cache; CursorData? _cache;
String png; String png;
int id; String id;
double Function(double)? hotxGetter; double Function(double)? hotxGetter;
double Function(double)? hotyGetter; double Function(double)? hotyGetter;
@@ -1614,13 +1739,22 @@ class PredefinedCursor {
init() { init() {
_image2 = img2.decodePng(base64Decode(png)); _image2 = img2.decodePng(base64Decode(png));
if (_image2 != null) { if (_image2 != null) {
// The png type of forbidden cursor image is `PngColorType.indexed`.
if (isWindows && id == kPreForbiddenCursorId) {
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
}
() async { () async {
final defaultImg = _image2!; final defaultImg = _image2!;
// This function is called only one time, no need to care about the performance. // This function is called only one time, no need to care about the performance.
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba); Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
_image?.dispose();
_image = await img.decodeImageFromPixels( _image = await img.decodeImageFromPixels(
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888); data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
if (_image == null) {
print("decodeImageFromPixels failed, pre-defined cursor $id");
return;
}
double scale = 1.0; double scale = 1.0;
if (isWindows) { if (isWindows) {
data = _image2!.getBytes(order: img2.ChannelOrder.bgra); data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
@@ -1648,36 +1782,69 @@ class PredefinedCursor {
class CursorModel with ChangeNotifier { class CursorModel with ChangeNotifier {
ui.Image? _image; ui.Image? _image;
final _images = <int, Tuple3<ui.Image, double, double>>{}; final _images = <String, Tuple3<ui.Image, double, double>>{};
CursorData? _cache; CursorData? _cache;
final _cacheMap = <int, CursorData>{}; final _cacheMap = <String, CursorData>{};
final _cacheKeys = <String>{}; final _cacheKeys = <String>{};
double _x = -10000; double _x = -10000;
double _y = -10000; double _y = -10000;
int _id = -1; // int.parse(evt['id']) may cause FormatException
// So we use String here.
String _id = "-1";
double _hotx = 0; double _hotx = 0;
double _hoty = 0; double _hoty = 0;
double _displayOriginX = 0; double _displayOriginX = 0;
double _displayOriginY = 0; double _displayOriginY = 0;
DateTime? _firstUpdateMouseTime; DateTime? _firstUpdateMouseTime;
Rect? _windowRect;
List<RemoteWindowCoords> _remoteWindowCoords = [];
bool gotMouseControl = true; bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now() DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec)); .subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
String peerId = ''; String peerId = '';
WeakReference<FFI> parent; WeakReference<FFI> parent;
// Only for mobile, touch mode
// To block touch event above the KeyHelpTools
//
// A better way is to not listen events from the KeyHelpTools.
// But we're now using a Container(child: Stack(...)) to wrap the KeyHelpTools,
// and the listener is on the Container.
Rect? _keyHelpToolsRect;
// `lastIsBlocked` is only used in common/widgets/remote_input.dart -> _RawTouchGestureDetectorRegionState -> onDoubleTap()
// Because onDoubleTap() doesn't have the `event` parameter, we can't get the touch event's position.
bool _lastIsBlocked = false;
double _yForKeyboardAdjust = 0;
keyHelpToolsVisibilityChanged(Rect? r) {
_keyHelpToolsRect = r;
if (r == null) {
_lastIsBlocked = false;
} else {
// Block the touch event is safe here.
// `lastIsBlocked` is only used in onDoubleTap() to block the touch event from the KeyHelpTools.
// `lastIsBlocked` will be set when the cursor is moving or touch somewhere else.
_lastIsBlocked = true;
}
_yForKeyboardAdjust = _y;
}
get lastIsBlocked => _lastIsBlocked;
ui.Image? get image => _image; ui.Image? get image => _image;
CursorData? get cache => _cache; CursorData? get cache => _cache;
double get x => _x - _displayOriginX; double get x => _x - _displayOriginX;
double get y => _y - _displayOriginY; double get y => _y - _displayOriginY;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
Offset get offset => Offset(_x, _y); Offset get offset => Offset(_x, _y);
double get hotx => _hotx; double get hotx => _hotx;
double get hoty => _hoty; double get hoty => _hoty;
set id(int id) => _id = id; set id(String id) => _id = id;
bool get isPeerControlProtected => bool get isPeerControlProtected =>
DateTime.now().difference(_lastPeerMouse).inMilliseconds < DateTime.now().difference(_lastPeerMouse).inMilliseconds <
@@ -1708,28 +1875,52 @@ class CursorModel with ChangeNotifier {
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale); return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
} }
get keyboardHeight => MediaQueryData.fromWindow(ui.window).viewInsets.bottom;
get scale => parent.target?.canvasModel.scale ?? 1.0;
double adjustForKeyboard() { double adjustForKeyboard() {
if (keyboardHeight < 100) {
return 0.0;
}
final m = MediaQueryData.fromWindow(ui.window); final m = MediaQueryData.fromWindow(ui.window);
var keyboardHeight = m.viewInsets.bottom;
final size = m.size; final size = m.size;
if (keyboardHeight < 100) return 0;
final s = parent.target?.canvasModel.scale ?? 1.0;
final thresh = (size.height - keyboardHeight) / 2; final thresh = (size.height - keyboardHeight) / 2;
var h = (_y - getVisibleRect().top) * s; // local physical display height final h = (_yForKeyboardAdjust - getVisibleRect().top) *
scale; // local physical display height
return h - thresh; return h - thresh;
} }
move(double x, double y) { // mobile Soft keyboard, block touch event from the KeyHelpTools
moveLocal(x, y); shouldBlock(double x, double y) {
parent.target?.inputModel.moveMouse(_x, _y); if (!(parent.target?.ffiModel.touchMode ?? false)) {
return false;
}
if (_keyHelpToolsRect == null) {
return false;
}
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
return true;
}
return false;
} }
moveLocal(double x, double y) { move(double x, double y) {
final scale = parent.target?.canvasModel.scale ?? 1.0; if (shouldBlock(x, y)) {
_lastIsBlocked = true;
return false;
}
_lastIsBlocked = false;
moveLocal(x, y, adjust: adjustForKeyboard());
parent.target?.inputModel.moveMouse(_x, _y);
return true;
}
moveLocal(double x, double y, {double adjust = 0}) {
final xoffset = parent.target?.canvasModel.x ?? 0; final xoffset = parent.target?.canvasModel.x ?? 0;
final yoffset = parent.target?.canvasModel.y ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0;
_x = (x - xoffset) / scale + _displayOriginX; _x = (x - xoffset) / scale + _displayOriginX;
_y = (y - yoffset) / scale + _displayOriginY; _y = (y - yoffset + adjust) / scale + _displayOriginY;
notifyListeners(); notifyListeners();
} }
@@ -1741,15 +1932,13 @@ class CursorModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
updatePan(double dx, double dy, bool touchMode) { updatePan(Offset delta, Offset localPosition, bool touchMode) {
if (touchMode) { if (touchMode) {
final scale = parent.target?.canvasModel.scale ?? 1.0; _handleTouchMode(delta, localPosition);
_x += dx / scale;
_y += dy / scale;
parent.target?.inputModel.moveMouse(_x, _y);
notifyListeners();
return; return;
} }
double dx = delta.dx;
double dy = delta.dy;
if (parent.target?.imageModel.image == null) return; if (parent.target?.imageModel.image == null) return;
final scale = parent.target?.canvasModel.scale ?? 1.0; final scale = parent.target?.canvasModel.scale ?? 1.0;
dx /= scale; dx /= scale;
@@ -1816,8 +2005,48 @@ class CursorModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool _isInCurrentWindow(double x, double y) {
final w = _windowRect!.width / devicePixelRatio;
final h = _windowRect!.width / devicePixelRatio;
return x >= 0 && y >= 0 && x <= w && y <= h;
}
_handleTouchMode(Offset delta, Offset localPosition) {
bool isMoved = false;
if (_remoteWindowCoords.isNotEmpty &&
_windowRect != null &&
!_isInCurrentWindow(localPosition.dx, localPosition.dy)) {
final coords = InputModel.findRemoteCoords(localPosition.dx,
localPosition.dy, _remoteWindowCoords, devicePixelRatio);
if (coords != null) {
double x2 =
(localPosition.dx - coords.relativeOffset.dx / devicePixelRatio) /
coords.canvas.scale;
double y2 =
(localPosition.dy - coords.relativeOffset.dy / devicePixelRatio) /
coords.canvas.scale;
x2 += coords.cursor.offset.dx;
y2 += coords.cursor.offset.dy;
parent.target?.inputModel.moveMouse(x2, y2);
isMoved = true;
}
}
if (!isMoved) {
final scale = parent.target?.canvasModel.scale ?? 1.0;
_x += delta.dx / scale;
_y += delta.dy / scale;
parent.target?.inputModel.moveMouse(_x, _y);
}
notifyListeners();
}
disposeImages() {
_images.forEach((_, v) => v.item1.dispose());
_images.clear();
}
updateCursorData(Map<String, dynamic> evt) async { updateCursorData(Map<String, dynamic> evt) async {
final id = int.parse(evt['id']); final id = evt['id'];
final hotx = double.parse(evt['hotx']); final hotx = double.parse(evt['hotx']);
final hoty = double.parse(evt['hoty']); final hoty = double.parse(evt['hoty']);
final width = int.parse(evt['width']); final width = int.parse(evt['width']);
@@ -1826,7 +2055,11 @@ class CursorModel with ChangeNotifier {
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
final image = await img.decodeImageFromPixels( final image = await img.decodeImageFromPixels(
rgba, width, height, ui.PixelFormat.rgba8888); rgba, width, height, ui.PixelFormat.rgba8888);
if (image == null) {
return;
}
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) { if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
_images[id]?.item1.dispose();
_images[id] = Tuple3(image, hotx, hoty); _images[id] = Tuple3(image, hotx, hoty);
} }
@@ -1838,7 +2071,7 @@ class CursorModel with ChangeNotifier {
Future<bool> _updateCache( Future<bool> _updateCache(
Uint8List rgba, Uint8List rgba,
ui.Image image, ui.Image image,
int id, String id,
double hotx, double hotx,
double hoty, double hoty,
int w, int w,
@@ -1941,7 +2174,7 @@ class CursorModel with ChangeNotifier {
_x = -10000; _x = -10000;
_x = -10000; _x = -10000;
_image = null; _image = null;
_images.clear(); disposeImages();
_clearCache(); _clearCache();
_cache = null; _cache = null;
@@ -1955,6 +2188,18 @@ class CursorModel with ChangeNotifier {
deleteCustomCursor(k); deleteCustomCursor(k);
} }
} }
trySetRemoteWindowCoords() {
Future.delayed(Duration.zero, () async {
_windowRect =
await InputModel.fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
});
}
clearRemoteWindowCoords() {
_windowRect = null;
_remoteWindowCoords.clear();
}
} }
class QualityMonitorData { class QualityMonitorData {
@@ -2172,6 +2417,7 @@ class FFI {
/// Mobile reuse FFI /// Mobile reuse FFI
void mobileReset() { void mobileReset() {
ffiModel.waitForFirstImage.value = true; ffiModel.waitForFirstImage.value = true;
ffiModel.isRefreshing = false;
ffiModel.waitForImageDialogShow.value = true; ffiModel.waitForImageDialogShow.value = true;
ffiModel.waitForImageTimer?.cancel(); ffiModel.waitForImageTimer?.cancel();
ffiModel.waitForImageTimer = null; ffiModel.waitForImageTimer = null;
@@ -2207,9 +2453,10 @@ class FFI {
cursorModel.peerId = id; cursorModel.peerId = id;
} }
final isNewPeer = tabWindowId == null;
// If tabWindowId != null, this session is a "tab -> window" one. // If tabWindowId != null, this session is a "tab -> window" one.
// Else this session is a new one. // Else this session is a new one.
if (tabWindowId == null) { if (isNewPeer) {
// ignore: unused_local_variable // ignore: unused_local_variable
final addRes = bind.sessionAddSync( final addRes = bind.sessionAddSync(
sessionId: sessionId, sessionId: sessionId,
@@ -2228,20 +2475,32 @@ class FFI {
'Unreachable, failed to add existed session to $id, the displays is null while display is $display'); 'Unreachable, failed to add existed session to $id, the displays is null while display is $display');
return; return;
} }
final addRes = bind.sessionAddExistedSync(id: id, sessionId: sessionId); final addRes = bind.sessionAddExistedSync(
id: id, sessionId: sessionId, displays: Int32List.fromList(displays));
if (addRes != '') { if (addRes != '') {
debugPrint( debugPrint(
'Unreachable, failed to add existed session to $id, $addRes'); 'Unreachable, failed to add existed session to $id, $addRes');
return; return;
} }
bind.sessionTryAddDisplay(
sessionId: sessionId, displays: Int32List.fromList(displays));
ffiModel.pi.currentDisplay = display; ffiModel.pi.currentDisplay = display;
} }
if (connType == ConnType.defaultConn && useTextureRender) { if (isDesktop && connType == ConnType.defaultConn) {
textureModel.updateCurrentDisplay(display ?? 0); textureModel.updateCurrentDisplay(display ?? 0);
} }
final stream = bind.sessionStart(sessionId: sessionId, id: id);
// CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions.
// Though the stream is returned immediately, the stream may not be ready.
// Any operations that depend on the stream should be carefully handled.
late final Stream<EventToUI> stream;
if (isNewPeer || display == null || displays == null) {
stream = bind.sessionStart(sessionId: sessionId, id: id);
} else {
// We have to put displays in `sessionStart()` to make sure the stream is ready
// and then the displays' capturing requests can be sent.
stream = bind.sessionStartWithDisplays(
sessionId: sessionId, id: id, displays: Int32List.fromList(displays));
}
if (isWeb) { if (isWeb) {
platformFFI.setRgbaCallback((int display, Uint8List data) { platformFFI.setRgbaCallback((int display, Uint8List data) {
onEvent2UIRgba(); onEvent2UIRgba();
@@ -2252,17 +2511,8 @@ class FFI {
final cb = ffiModel.startEventListener(sessionId, id); final cb = ffiModel.startEventListener(sessionId, id);
// Force refresh displays. imageModel.updateUserTextureRender();
// The controlled side may not refresh the image when the (peer,display) is already subscribed.
if (displays != null) {
for (final display in displays) {
bind.sessionRefresh(sessionId: sessionId, display: display);
}
}
final hasPixelBufferTextureRender = bind.mainHasPixelbufferTextureRender();
final hasGpuTextureRender = bind.mainHasGpuTextureRender(); final hasGpuTextureRender = bind.mainHasGpuTextureRender();
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false); final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
// Preserved for the rgba data. // Preserved for the rgba data.
stream.listen((message) { stream.listen((message) {
@@ -2284,8 +2534,11 @@ class FFI {
debugPrint('Unreachable, the cached data cannot be decoded.'); debugPrint('Unreachable, the cached data cannot be decoded.');
return; return;
} }
ffiModel.setPermissions(data.permissions);
await ffiModel.handleCachedPeerData(data, id); await ffiModel.handleCachedPeerData(data, id);
await sessionRefreshVideo(sessionId, ffiModel.pi); await sessionRefreshVideo(sessionId, ffiModel.pi);
await bind.sessionRequestNewDisplayInitMsgs(
sessionId: sessionId, display: ffiModel.pi.currentDisplay);
}); });
isToNewWindowNotified.value = true; isToNewWindowNotified.value = true;
} }
@@ -2308,20 +2561,23 @@ class FFI {
} }
} else if (message is EventToUI_Rgba) { } else if (message is EventToUI_Rgba) {
final display = message.field0; final display = message.field0;
if (hasPixelBufferTextureRender) { if (imageModel.useTextureRender || ffiModel.pi.forceTextureRender) {
debugPrint("EventToUI_Rgba display:$display"); //debugPrint("EventToUI_Rgba display:$display");
textureModel.setTextureType(display: display, gpuTexture: false); textureModel.setTextureType(display: display, gpuTexture: false);
onEvent2UIRgba(); onEvent2UIRgba();
} else { } else {
// Fetch the image buffer from rust codes. // Fetch the image buffer from rust codes.
final sz = platformFFI.getRgbaSize(sessionId, display); final sz = platformFFI.getRgbaSize(sessionId, display);
if (sz == 0) { if (sz == 0) {
platformFFI.nextRgba(sessionId, display);
return; return;
} }
final rgba = platformFFI.getRgba(sessionId, display, sz); final rgba = platformFFI.getRgba(sessionId, display, sz);
if (rgba != null) { if (rgba != null) {
onEvent2UIRgba(); onEvent2UIRgba();
imageModel.onRgba(display, rgba); imageModel.onRgba(display, rgba);
} else {
platformFFI.nextRgba(sessionId, display);
} }
} }
} else if (message is EventToUI_Texture) { } else if (message is EventToUI_Texture) {
@@ -2490,38 +2746,48 @@ class PeerInfo with ChangeNotifier {
bool get isInstalled => bool get isInstalled =>
platform != kPeerPlatformWindows || platform != kPeerPlatformWindows ||
platformAdditions[kPlatformAdditionsIsInstalled] == true; platformAdditions[kPlatformAdditionsIsInstalled] == true;
List<int> get virtualDisplays => List<int>.from( List<int> get RustDeskVirtualDisplays => List<int>.from(
platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []); platformAdditions[kPlatformAdditionsRustDeskVirtualDisplays] ?? []);
int get amyuniVirtualDisplayCount =>
platformAdditions[kPlatformAdditionsAmyuniVirtualDisplays] ?? 0;
bool get isSupportMultiDisplay => bool get isSupportMultiDisplay =>
(isDesktop || isWebDesktop) && isSupportMultiUiSession; (isDesktop || isWebDesktop) && isSupportMultiUiSession;
bool get forceTextureRender => currentDisplay == kAllDisplayValue;
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false; bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
Display? tryGetDisplay() { bool get isRustDeskIdd =>
platformAdditions[kPlatformAdditionsIddImpl] == 'rustdesk_idd';
bool get isAmyuniIdd =>
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
Display? tryGetDisplay({int? display}) {
if (displays.isEmpty) { if (displays.isEmpty) {
return null; return null;
} }
if (currentDisplay == kAllDisplayValue) { display ??= currentDisplay;
if (display == kAllDisplayValue) {
return displays[0]; return displays[0];
} else { } else {
if (currentDisplay > 0 && currentDisplay < displays.length) { if (display > 0 && display < displays.length) {
return displays[currentDisplay]; return displays[display];
} else { } else {
return displays[0]; return displays[0];
} }
} }
} }
Display? tryGetDisplayIfNotAllDisplay() { Display? tryGetDisplayIfNotAllDisplay({int? display}) {
if (displays.isEmpty) { if (displays.isEmpty) {
return null; return null;
} }
if (currentDisplay == kAllDisplayValue) { display ??= currentDisplay;
if (display == kAllDisplayValue) {
return null; return null;
} }
if (currentDisplay >= 0 && currentDisplay < displays.length) { if (display >= 0 && display < displays.length) {
return displays[currentDisplay]; return displays[display];
} else { } else {
return null; return null;
} }
@@ -2545,6 +2811,12 @@ class PeerInfo with ChangeNotifier {
} }
return 1.0; return 1.0;
} }
Rect? getDisplayRect(int display) {
final d = tryGetDisplayIfNotAllDisplay(display: display);
if (d == null) return null;
return Rect.fromLTWH(d.x, d.y, d.width.toDouble(), d.height.toDouble());
}
} }
const canvasKey = 'canvas'; const canvasKey = 'canvas';

View File

@@ -132,9 +132,10 @@ class PlatformFFI {
_ffiBind = RustdeskImpl(dylib); _ffiBind = RustdeskImpl(dylib);
if (isLinux) { if (isLinux) {
// Start a dbus service, no need to await if (isMain) {
_ffiBind.mainStartDbusServer(); // Start a dbus service for uri links, no need to await
_ffiBind.mainStartPa(); _ffiBind.mainStartDbusServer();
}
} else if (isMacOS && isMain) { } else if (isMacOS && isMain) {
// Start ipc service for uri links. // Start ipc service for uri links.
_ffiBind.mainStartIpcUrlServer(); _ffiBind.mainStartIpcUrlServer();
@@ -198,7 +199,10 @@ class PlatformFFI {
await _ffiBind.mainDeviceId(id: id); await _ffiBind.mainDeviceId(id: id);
await _ffiBind.mainDeviceName(name: name); await _ffiBind.mainDeviceName(name: name);
await _ffiBind.mainSetHomeDir(home: _homeDir); await _ffiBind.mainSetHomeDir(home: _homeDir);
await _ffiBind.mainInit(appDir: _dir); await _ffiBind.mainInit(
appDir: _dir,
customClientConfig: '',
);
} catch (e) { } catch (e) {
debugPrintStack(label: 'initialize failed: $e'); debugPrintStack(label: 'initialize failed: $e');
} }

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.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/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -22,9 +23,6 @@ class PeerTabModel with ChangeNotifier {
int get currentTab => _currentTab; int get currentTab => _currentTab;
int _currentTab = 0; // index in tabNames int _currentTab = 0; // index in tabNames
static const int maxTabCount = 5; static const int maxTabCount = 5;
static const String kPeerTabIndex = 'peer-tab-index';
static const String kPeerTabOrder = 'peer-tab-order';
static const String kPeerTabVisible = 'peer-tab-visible';
static const List<String> tabNames = [ static const List<String> tabNames = [
'Recent sessions', 'Recent sessions',
'Favorites', 'Favorites',
@@ -44,7 +42,7 @@ class PeerTabModel with ChangeNotifier {
true, true,
!isWeb, !isWeb,
!(bind.isDisableAb() || bind.isDisableAccount()), !(bind.isDisableAb() || bind.isDisableAccount()),
!bind.isDisableAccount(), !(bind.isDisableGroupPanel() || bind.isDisableAccount()),
]); ]);
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false); final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
List<bool> get isVisibleEnabled => () { List<bool> get isVisibleEnabled => () {
@@ -72,7 +70,7 @@ class PeerTabModel with ChangeNotifier {
PeerTabModel(this.parent) { PeerTabModel(this.parent) {
// visible // visible
try { try {
final option = bind.getLocalFlutterOption(k: kPeerTabVisible); final option = bind.getLocalFlutterOption(k: kOptionPeerTabVisible);
if (option.isNotEmpty) { if (option.isNotEmpty) {
List<dynamic> decodeList = jsonDecode(option); List<dynamic> decodeList = jsonDecode(option);
if (decodeList.length == _isVisible.length) { if (decodeList.length == _isVisible.length) {
@@ -88,7 +86,7 @@ class PeerTabModel with ChangeNotifier {
} }
// order // order
try { try {
final option = bind.getLocalFlutterOption(k: kPeerTabOrder); final option = bind.getLocalFlutterOption(k: kOptionPeerTabOrder);
if (option.isNotEmpty) { if (option.isNotEmpty) {
List<dynamic> decodeList = jsonDecode(option); List<dynamic> decodeList = jsonDecode(option);
if (decodeList.length == maxTabCount) { if (decodeList.length == maxTabCount) {
@@ -112,7 +110,7 @@ class PeerTabModel with ChangeNotifier {
} }
// init currentTab // init currentTab
_currentTab = _currentTab =
int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0; int.tryParse(bind.getLocalFlutterOption(k: kOptionPeerTabIndex)) ?? 0;
if (_currentTab < 0 || _currentTab >= maxTabCount) { if (_currentTab < 0 || _currentTab >= maxTabCount) {
_currentTab = 0; _currentTab = 0;
} }
@@ -222,7 +220,7 @@ class PeerTabModel with ChangeNotifier {
} }
try { try {
bind.setLocalFlutterOption( bind.setLocalFlutterOption(
k: kPeerTabVisible, v: jsonEncode(_isVisible)); k: kOptionPeerTabVisible, v: jsonEncode(_isVisible));
} catch (_) {} } catch (_) {}
notifyListeners(); notifyListeners();
} }
@@ -258,7 +256,7 @@ class PeerTabModel with ChangeNotifier {
for (int i = 0; i < list.length; i++) { for (int i = 0; i < list.length; i++) {
orders[i] = list[i]; orders[i] = list[i];
} }
bind.setLocalFlutterOption(k: kPeerTabOrder, v: jsonEncode(orders)); bind.setLocalFlutterOption(k: kOptionPeerTabOrder, v: jsonEncode(orders));
notifyListeners(); notifyListeners();
} }
} }

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -77,7 +78,7 @@ class ServerModel with ChangeNotifier {
String get approveMode => _approveMode; String get approveMode => _approveMode;
setVerificationMethod(String method) async { setVerificationMethod(String method) async {
await bind.mainSetOption(key: "verification-method", value: method); await bind.mainSetOption(key: kOptionVerificationMethod, value: method);
/* /*
if (method != kUsePermanentPassword) { if (method != kUsePermanentPassword) {
await bind.mainSetOption( await bind.mainSetOption(
@@ -99,7 +100,7 @@ class ServerModel with ChangeNotifier {
} }
setApproveMode(String mode) async { setApproveMode(String mode) async {
await bind.mainSetOption(key: 'approve-mode', value: mode); await bind.mainSetOption(key: kOptionApproveMode, value: mode);
/* /*
if (mode != 'password') { if (mode != 'password') {
await bind.mainSetOption( await bind.mainSetOption(
@@ -125,8 +126,8 @@ class ServerModel with ChangeNotifier {
/* /*
// initital _hideCm at startup // initital _hideCm at startup
final verificationMethod = final verificationMethod =
bind.mainGetOptionSync(key: "verification-method"); bind.mainGetOptionSync(key: kOptionVerificationMethod);
final approveMode = bind.mainGetOptionSync(key: 'approve-mode'); final approveMode = bind.mainGetOptionSync(key: kOptionApproveMode);
_hideCm = option2bool( _hideCm = option2bool(
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm')); 'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
if (!(approveMode == 'password' && if (!(approveMode == 'password' &&
@@ -187,18 +188,19 @@ class ServerModel with ChangeNotifier {
if (androidVersion < 30 || if (androidVersion < 30 ||
!await AndroidPermissionManager.check(kRecordAudio)) { !await AndroidPermissionManager.check(kRecordAudio)) {
_audioOk = false; _audioOk = false;
bind.mainSetOption(key: "enable-audio", value: "N"); bind.mainSetOption(key: kOptionEnableAudio, value: "N");
} else { } else {
final audioOption = await bind.mainGetOption(key: 'enable-audio'); final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
_audioOk = audioOption.isEmpty; _audioOk = audioOption.isEmpty;
} }
// file // file
if (!await AndroidPermissionManager.check(kManageExternalStorage)) { if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
_fileOk = false; _fileOk = false;
bind.mainSetOption(key: "enable-file-transfer", value: "N"); bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
} else { } else {
final fileOption = await bind.mainGetOption(key: 'enable-file-transfer'); final fileOption =
await bind.mainGetOption(key: kOptionEnableFileTransfer);
_fileOk = fileOption.isEmpty; _fileOk = fileOption.isEmpty;
} }
@@ -209,10 +211,10 @@ class ServerModel with ChangeNotifier {
var update = false; var update = false;
final temporaryPassword = await bind.mainGetTemporaryPassword(); final temporaryPassword = await bind.mainGetTemporaryPassword();
final verificationMethod = final verificationMethod =
await bind.mainGetOption(key: "verification-method"); await bind.mainGetOption(key: kOptionVerificationMethod);
final temporaryPasswordLength = final temporaryPasswordLength =
await bind.mainGetOption(key: "temporary-password-length"); await bind.mainGetOption(key: "temporary-password-length");
final approveMode = await bind.mainGetOption(key: 'approve-mode'); final approveMode = await bind.mainGetOption(key: kOptionApproveMode);
/* /*
var hideCm = option2bool( var hideCm = option2bool(
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm')); 'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
@@ -225,8 +227,7 @@ class ServerModel with ChangeNotifier {
_approveMode = approveMode; _approveMode = approveMode;
update = true; update = true;
} }
var stopped = option2bool( var stopped = await mainGetBoolOption(kOptionStopService);
"stop-service", await bind.mainGetOption(key: "stop-service"));
final oldPwdText = _serverPasswd.text; final oldPwdText = _serverPasswd.text;
if (stopped || if (stopped ||
verificationMethod == kUsePermanentPassword || verificationMethod == kUsePermanentPassword ||
@@ -283,7 +284,8 @@ class ServerModel with ChangeNotifier {
} }
_audioOk = !_audioOk; _audioOk = !_audioOk;
bind.mainSetOption(key: "enable-audio", value: _audioOk ? '' : 'N'); bind.mainSetOption(
key: kOptionEnableAudio, value: _audioOk ? defaultOptionYes : 'N');
notifyListeners(); notifyListeners();
} }
@@ -302,7 +304,9 @@ class ServerModel with ChangeNotifier {
} }
_fileOk = !_fileOk; _fileOk = !_fileOk;
bind.mainSetOption(key: "enable-file-transfer", value: _fileOk ? '' : 'N'); bind.mainSetOption(
key: kOptionEnableFileTransfer,
value: _fileOk ? defaultOptionYes : 'N');
notifyListeners(); notifyListeners();
} }
@@ -312,7 +316,7 @@ class ServerModel with ChangeNotifier {
} }
if (_inputOk) { if (_inputOk) {
parent.target?.invokeMethod("stop_input"); parent.target?.invokeMethod("stop_input");
bind.mainSetOption(key: "enable-keyboard", value: 'N'); bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
} else { } else {
if (parent.target != null) { if (parent.target != null) {
/// the result of toggle-on depends on user actions in the settings page. /// the result of toggle-on depends on user actions in the settings page.
@@ -322,6 +326,34 @@ class ServerModel with ChangeNotifier {
} }
} }
Future<bool> checkRequestNotificationPermission() async {
debugPrint("androidVersion $androidVersion");
if (androidVersion < 33) {
return true;
}
if (await AndroidPermissionManager.check(kAndroid13Notification)) {
debugPrint("notification permission already granted");
return true;
}
var res = await AndroidPermissionManager.request(kAndroid13Notification);
debugPrint("notification permission request result: $res");
return res;
}
Future<bool> checkFloatingWindowPermission() async {
debugPrint("androidVersion $androidVersion");
if (androidVersion < 23) {
return false;
}
if (await AndroidPermissionManager.check(kSystemAlertWindow)) {
debugPrint("alert window permission already granted");
return true;
}
var res = await AndroidPermissionManager.request(kSystemAlertWindow);
debugPrint("alert window permission request result: $res");
return res;
}
/// Toggle the screen sharing service. /// Toggle the screen sharing service.
toggleService() async { toggleService() async {
if (_isStart) { if (_isStart) {
@@ -348,6 +380,13 @@ class ServerModel with ChangeNotifier {
stopService(); stopService();
} }
} else { } else {
await checkRequestNotificationPermission();
if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
await checkFloatingWindowPermission();
}
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
await AndroidPermissionManager.request(kManageExternalStorage);
}
final res = await parent.target?.dialogManager final res = await parent.target?.dialogManager
.show<bool>((setState, close, context) { .show<bool>((setState, close, context) {
submit() => close(true); submit() => close(true);
@@ -383,7 +422,7 @@ class ServerModel with ChangeNotifier {
await bind.mainStartService(); await bind.mainStartService();
updateClientState(); updateClientState();
if (isAndroid) { if (isAndroid) {
WakelockPlus.enable(); androidUpdatekeepScreenOn();
} }
} }
@@ -430,7 +469,9 @@ class ServerModel with ChangeNotifier {
break; break;
case "input": case "input":
if (_inputOk != value) { if (_inputOk != value) {
bind.mainSetOption(key: "enable-keyboard", value: value ? '' : 'N'); bind.mainSetOption(
key: kOptionEnableKeyboard,
value: value ? defaultOptionYes : 'N');
} }
_inputOk = value; _inputOk = value;
break; break;
@@ -474,6 +515,7 @@ class ServerModel with ChangeNotifier {
} }
if (_clients.length != oldClientLenght) { if (_clients.length != oldClientLenght) {
notifyListeners(); notifyListeners();
if (isAndroid) androidUpdatekeepScreenOn();
} }
} }
@@ -508,6 +550,7 @@ class ServerModel with ChangeNotifier {
scrollToBottom(); scrollToBottom();
notifyListeners(); notifyListeners();
if (isAndroid && !client.authorized) showLoginDialog(client); if (isAndroid && !client.authorized) showLoginDialog(client);
if (isAndroid) androidUpdatekeepScreenOn();
} catch (e) { } catch (e) {
debugPrint("Failed to call loginRequest,error:$e"); debugPrint("Failed to call loginRequest,error:$e");
} }
@@ -535,37 +578,60 @@ class ServerModel with ChangeNotifier {
} }
void showLoginDialog(Client client) { void showLoginDialog(Client client) {
showClientDialog(
client,
client.isFileTransfer ? "File Connection" : "Screen Connection",
'Do you accept?',
'android_new_connection_tip',
() => sendLoginResponse(client, false),
() => sendLoginResponse(client, true),
);
}
handleVoiceCall(Client client, bool accept) {
parent.target?.invokeMethod("cancel_notification", client.id);
bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
}
showVoiceCallDialog(Client client) {
showClientDialog(
client,
'Voice call',
'Do you accept?',
'android_new_voice_call_tip',
() => handleVoiceCall(client, false),
() => handleVoiceCall(client, true),
);
}
showClientDialog(Client client, String title, String contentTitle,
String content, VoidCallback onCancel, VoidCallback onSubmit) {
parent.target?.dialogManager.show((setState, close, context) { parent.target?.dialogManager.show((setState, close, context) {
cancel() { cancel() {
sendLoginResponse(client, false); onCancel();
close(); close();
} }
submit() { submit() {
sendLoginResponse(client, true); onSubmit();
close(); close();
} }
return CustomAlertDialog( return CustomAlertDialog(
title: title:
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text(translate( Text(translate(title)),
client.isFileTransfer ? "File Connection" : "Screen Connection")), IconButton(onPressed: close, icon: const Icon(Icons.close))
IconButton(
onPressed: () {
close();
},
icon: const Icon(Icons.close))
]), ]),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(translate("Do you accept?")), Text(translate(contentTitle)),
ClientInfo(client), ClientInfo(client),
Text( Text(
translate("android_new_connection_tip"), translate(content),
style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium, style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
), ),
], ],
@@ -605,6 +671,7 @@ class ServerModel with ChangeNotifier {
final index = _clients.indexOf(client); final index = _clients.indexOf(client);
tabController.remove(index); tabController.remove(index);
_clients.remove(client); _clients.remove(client);
if (isAndroid) androidUpdatekeepScreenOn();
} }
} }
@@ -628,6 +695,7 @@ class ServerModel with ChangeNotifier {
if (desktopType == DesktopType.cm && _clients.isEmpty) { if (desktopType == DesktopType.cm && _clients.isEmpty) {
hideCmWindow(); hideCmWindow();
} }
if (isAndroid) androidUpdatekeepScreenOn();
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
debugPrint("onClientRemove failed,error:$e"); debugPrint("onClientRemove failed,error:$e");
@@ -639,6 +707,7 @@ class ServerModel with ChangeNotifier {
_clients.map((client) => bind.cmCloseConnection(connId: client.id))); _clients.map((client) => bind.cmCloseConnection(connId: client.id)));
_clients.clear(); _clients.clear();
tabController.state.value.tabs.clear(); tabController.state.value.tabs.clear();
if (isAndroid) androidUpdatekeepScreenOn();
} }
void jumpTo(int id) { void jumpTo(int id) {
@@ -661,10 +730,14 @@ class ServerModel with ChangeNotifier {
_clients[index].inVoiceCall = client.inVoiceCall; _clients[index].inVoiceCall = client.inVoiceCall;
_clients[index].incomingVoiceCall = client.incomingVoiceCall; _clients[index].incomingVoiceCall = client.incomingVoiceCall;
if (client.incomingVoiceCall) { if (client.incomingVoiceCall) {
// Has incoming phone call, let's set the window on top. if (isAndroid) {
Future.delayed(Duration.zero, () { showVoiceCallDialog(client);
windowOnTop(null); } else {
}); // Has incoming phone call, let's set the window on top.
Future.delayed(Duration.zero, () {
windowOnTop(null);
});
}
} }
notifyListeners(); notifyListeners();
} }
@@ -672,6 +745,27 @@ class ServerModel with ChangeNotifier {
debugPrint("updateVoiceCallState failed: $e"); debugPrint("updateVoiceCallState failed: $e");
} }
} }
void androidUpdatekeepScreenOn() async {
if (!isAndroid) return;
var floatingWindowDisabled =
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
!await AndroidPermissionManager.check(kSystemAlertWindow);
final keepScreenOn = floatingWindowDisabled
? KeepScreenOn.never
: optionToKeepScreenOn(
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
final on = ((keepScreenOn == KeepScreenOn.serviceOn) && _isStart) ||
(keepScreenOn == KeepScreenOn.duringControlled &&
_clients.map((e) => !e.disconnected).isNotEmpty);
if (on != await WakelockPlus.enabled) {
if (on) {
WakelockPlus.enable();
} else {
WakelockPlus.disable();
}
}
}
} }
enum ClientType { enum ClientType {

View File

@@ -14,12 +14,11 @@ 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(kWindowEdgeSize); final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize);
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;
// Only used for macOS final RxBool isFocused = false.obs;
bool? closeOnFullscreen;
String _inputSource = ''; String _inputSource = '';
@@ -55,8 +54,7 @@ class StateGlobal {
if (!_fullscreen.isTrue) { if (!_fullscreen.isTrue) {
if (isMaximized.value != v) { if (isMaximized.value != v) {
isMaximized.value = v; isMaximized.value = v;
_resizeEdgeSize.value = refreshResizeEdgeSize();
isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
} }
if (!isMacOS) { if (!isMacOS) {
_windowBorderWidth.value = v ? 0 : kWindowBorderWidth; _windowBorderWidth.value = v ? 0 : kWindowBorderWidth;
@@ -70,11 +68,7 @@ class StateGlobal {
if (_fullscreen.value != v) { if (_fullscreen.value != v) {
_fullscreen.value = v; _fullscreen.value = v;
_showTabBar.value = !_fullscreen.value; _showTabBar.value = !_fullscreen.value;
_resizeEdgeSize.value = fullscreen.isTrue refreshResizeEdgeSize();
? kFullScreenEdgeSize
: isMaximized.isTrue
? kMaximizeEdgeSize
: kWindowEdgeSize;
print( print(
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); "fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; _windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
@@ -95,6 +89,12 @@ class StateGlobal {
} }
} }
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
? kFullScreenEdgeSize
: isMaximized.isTrue
? kMaximizeEdgeSize
: windowEdgeSize;
String getInputSource({bool force = false}) { String getInputSource({bool force = false}) {
if (force || _inputSource.isEmpty) { if (force || _inputSource.isEmpty) {
_inputSource = bind.mainGetInputSource(); _inputSource = bind.mainGetInputSource();
@@ -112,4 +112,5 @@ class StateGlobal {
static final StateGlobal instance = StateGlobal._(); static final StateGlobal instance = StateGlobal._();
} }
// This final variable is initialized when the first time it is accessed.
final stateGlobal = StateGlobal.instance; final stateGlobal = StateGlobal.instance;

View File

@@ -6,9 +6,9 @@ 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/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import '../common.dart'; import '../common.dart';
import '../utils/http_service.dart' as http;
import 'model.dart'; import 'model.dart';
import 'platform_model.dart'; import 'platform_model.dart';
@@ -136,7 +136,6 @@ class UserModel {
Future<LoginResponse> login(LoginRequest loginRequest) async { Future<LoginResponse> login(LoginRequest loginRequest) async {
final url = await bind.mainGetApiServer(); final url = await bind.mainGetApiServer();
final resp = await http.post(Uri.parse('$url/api/login'), final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(loginRequest.toJson())); body: jsonEncode(loginRequest.toJson()));
final Map<String, dynamic> body; final Map<String, dynamic> body;

View File

@@ -0,0 +1,115 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../models/platform_model.dart';
export 'package:http/http.dart' show Response;
enum HttpMethod { get, post, put, delete }
class HttpService {
Future<http.Response> sendRequest(
Uri url,
HttpMethod method, {
Map<String, String>? headers,
dynamic body,
}) async {
headers ??= {'Content-Type': 'application/json'};
// Determine if there is currently a proxy setting, and if so, use FFI to call the Rust HTTP method.
final isProxy = await bind.mainGetProxyStatus();
if (!isProxy) {
return await _pollFultterHttp(url, method, headers: headers, body: body);
}
String headersJson = jsonEncode(headers);
String methodName = method.toString().split('.').last;
await bind.mainHttpRequest(
url: url.toString(),
method: methodName.toLowerCase(),
body: body,
header: headersJson);
var resJson = await _pollForResponse(url.toString());
return _parseHttpResponse(resJson);
}
Future<http.Response> _pollFultterHttp(
Uri url,
HttpMethod method, {
Map<String, String>? headers,
dynamic body,
}) async {
var response = http.Response('', 400);
switch (method) {
case HttpMethod.get:
response = await http.get(url, headers: headers);
break;
case HttpMethod.post:
response = await http.post(url, headers: headers, body: body);
break;
case HttpMethod.put:
response = await http.put(url, headers: headers, body: body);
break;
case HttpMethod.delete:
response = await http.delete(url, headers: headers, body: body);
break;
default:
throw Exception('Unsupported HTTP method');
}
return response;
}
Future<String> _pollForResponse(String url) async {
String? responseJson = " ";
while (responseJson == " ") {
responseJson = await bind.mainGetHttpStatus(url: url);
if (responseJson == null) {
throw Exception('The HTTP request failed');
}
if (responseJson == " ") {
await Future.delayed(const Duration(milliseconds: 100));
}
}
return responseJson!;
}
http.Response _parseHttpResponse(String responseJson) {
try {
var parsedJson = jsonDecode(responseJson);
String body = parsedJson['body'];
Map<String, String> headers = {};
for (var key in parsedJson['headers'].keys) {
headers[key] = parsedJson['headers'][key];
}
int statusCode = parsedJson['status_code'];
return http.Response(body, statusCode, headers: headers);
} catch (e) {
throw Exception('Failed to parse response: $e');
}
}
}
Future<http.Response> get(Uri url, {Map<String, String>? headers}) async {
return await HttpService().sendRequest(url, HttpMethod.get, headers: headers);
}
Future<http.Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.post, body: body, headers: headers);
}
Future<http.Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.put, body: body, headers: headers);
}
Future<http.Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.delete, body: body, headers: headers);
}

View File

@@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
Future<ui.Image> decodeImageFromPixels( Future<ui.Image?> decodeImageFromPixels(
Uint8List pixels, Uint8List pixels,
int width, int width,
int height, int height,
@@ -13,41 +13,82 @@ Future<ui.Image> decodeImageFromPixels(
int? rowBytes, int? rowBytes,
int? targetWidth, int? targetWidth,
int? targetHeight, int? targetHeight,
VoidCallback? onPixelsCopied, 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)) {
print("not allow upscaling but targetWidth > width");
onPixelsCopied?.call();
return null;
}
} }
if (targetHeight != null) { if (targetHeight != null) {
assert(allowUpscaling || targetHeight <= height); assert(allowUpscaling || targetHeight <= height);
} if (!(allowUpscaling || targetHeight <= height)) {
print("not allow upscaling but targetHeight > height");
final ui.ImmutableBuffer buffer = onPixelsCopied?.call();
await ui.ImmutableBuffer.fromUint8List(pixels); return null;
onPixelsCopied?.call();
final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
buffer,
width: width,
height: height,
rowBytes: rowBytes,
pixelFormat: format,
);
if (!allowUpscaling) {
if (targetWidth != null && targetWidth > descriptor.width) {
targetWidth = descriptor.width;
}
if (targetHeight != null && targetHeight > descriptor.height) {
targetHeight = descriptor.height;
} }
} }
final ui.Codec codec = await descriptor.instantiateCodec( final ui.ImmutableBuffer buffer;
targetWidth: targetWidth, try {
targetHeight: targetHeight, buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
); onPixelsCopied?.call();
} catch (e) {
onPixelsCopied?.call();
return null;
}
final ui.ImageDescriptor descriptor;
try {
descriptor = ui.ImageDescriptor.raw(
buffer,
width: width,
height: height,
rowBytes: rowBytes,
pixelFormat: format,
);
if (!allowUpscaling) {
if (targetWidth != null && targetWidth > descriptor.width) {
targetWidth = descriptor.width;
}
if (targetHeight != null && targetHeight > descriptor.height) {
targetHeight = descriptor.height;
}
}
} catch (e) {
print("ImageDescriptor.raw failed: $e");
buffer.dispose();
return null;
}
final ui.Codec codec;
try {
codec = await descriptor.instantiateCodec(
targetWidth: targetWidth,
targetHeight: targetHeight,
);
} catch (e) {
print("instantiateCodec failed: $e");
buffer.dispose();
descriptor.dispose();
return null;
}
final ui.FrameInfo frameInfo;
try {
frameInfo = await codec.getNextFrame();
} catch (e) {
print("getNextFrame failed: $e");
codec.dispose();
buffer.dispose();
descriptor.dispose();
return null;
}
final ui.FrameInfo frameInfo = await codec.getNextFrame();
codec.dispose(); codec.dispose();
buffer.dispose(); buffer.dispose();
descriptor.dispose(); descriptor.dispose();

View File

@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/input_model.dart';
/// must keep the order /// must keep the order
// ignore: constant_identifier_names // ignore: constant_identifier_names
@@ -172,7 +174,9 @@ class RustDeskMultiWindowManager {
windowId: windowId, peerId: remoteId); windowId: windowId, peerId: remoteId);
} }
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg); await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
WindowController.fromWindowId(windowId).show(); if (methodName != kWindowEventNewRemoteDesktop) {
WindowController.fromWindowId(windowId).show();
}
registerActiveWindow(windowId); registerActiveWindow(windowId);
return MultiWindowCallResult(windowId, null); return MultiWindowCallResult(windowId, null);
} }
@@ -332,10 +336,10 @@ class RustDeskMultiWindowManager {
} }
Future<void> closeAllSubWindows() async { Future<void> closeAllSubWindows() async {
await Future.wait(WindowType.values.map((e) => closeWindows(e))); await Future.wait(WindowType.values.map((e) => _closeWindows(e)));
} }
Future<void> closeWindows(WindowType type) async { Future<void> _closeWindows(WindowType type) async {
if (type == WindowType.Main) { if (type == WindowType.Main) {
// skip main window, use window manager instead // skip main window, use window manager instead
return; return;
@@ -343,7 +347,7 @@ class RustDeskMultiWindowManager {
List<int> windows = []; List<int> windows = [];
try { try {
windows = await DesktopMultiWindow.getAllSubWindowIds(); windows = _findWindowsByType(type);
} catch (e) { } catch (e) {
debugPrint('Failed to getAllSubWindowIds of $type, $e'); debugPrint('Failed to getAllSubWindowIds of $type, $e');
return; return;
@@ -353,14 +357,9 @@ class RustDeskMultiWindowManager {
return; return;
} }
for (final wId in windows) { for (final wId in windows) {
debugPrint("closing multi window: ${type.toString()}"); debugPrint("closing multi window, type: ${type.toString()} id: $wId");
await saveWindowPosition(type, windowId: wId); await saveWindowPosition(type, windowId: wId);
try { try {
// final ids = await DesktopMultiWindow.getAllSubWindowIds();
// if (!ids.contains(wId)) {
// // no such window already
// return;
// }
await WindowController.fromWindowId(wId).setPreventClose(false); await WindowController.fromWindowId(wId).setPreventClose(false);
await WindowController.fromWindowId(wId).close(); await WindowController.fromWindowId(wId).close();
_activeWindows.remove(wId); _activeWindows.remove(wId);
@@ -369,7 +368,6 @@ class RustDeskMultiWindowManager {
return; return;
} }
} }
await _notifyActiveWindow();
clearWindowType(type); clearWindowType(type);
} }
@@ -402,14 +400,6 @@ class RustDeskMultiWindowManager {
await _notifyActiveWindow(); await _notifyActiveWindow();
} }
Future<void> destroyWindow(int windowId) async {
await WindowController.fromWindowId(windowId).setPreventClose(false);
await WindowController.fromWindowId(windowId).close();
_remoteDesktopWindows.remove(windowId);
_fileTransferWindows.remove(windowId);
_portForwardWindows.remove(windowId);
}
/// Remove active window which has [`windowId`] /// Remove active window which has [`windowId`]
/// ///
/// [Availability] /// [Availability]
@@ -431,6 +421,39 @@ class RustDeskMultiWindowManager {
void unregisterActiveWindowListener(AsyncCallback callback) { void unregisterActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.remove(callback); _windowActiveCallbacks.remove(callback);
} }
// This function is called from the main window.
// It will query the active remote windows to get their coords.
Future<List<String>> getOtherRemoteWindowCoords(int wId) async {
List<String> coords = [];
for (final windowId in _remoteDesktopWindows) {
if (windowId != wId) {
if (_activeWindows.contains(windowId)) {
final res = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventRemoteWindowCoords, '');
if (res != null) {
coords.add(res);
}
}
}
}
return coords;
}
// This function is called from one remote window.
// Only the main window knows `_remoteDesktopWindows` and `_activeWindows`.
// So we need to call the main window to get the other remote windows' coords.
Future<List<RemoteWindowCoords>> getOtherRemoteWindowCoordsFromMain() async {
List<RemoteWindowCoords> coords = [];
// Call the main window to get the coords of other remote windows.
String res = await DesktopMultiWindow.invokeMethod(
kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString());
List<dynamic> list = jsonDecode(res);
for (var item in list) {
coords.add(RemoteWindowCoords.fromJson(jsonDecode(item)));
}
return coords;
}
} }
final rustDeskWinManager = RustDeskMultiWindowManager.instance; final rustDeskWinManager = RustDeskMultiWindowManager.instance;

View File

@@ -59,15 +59,13 @@ class RustdeskImpl {
} }
String sessionAddExistedSync( String sessionAddExistedSync(
{required String id, required UuidValue sessionId, dynamic hint}) { {required String id,
required UuidValue sessionId,
required Int32List displays,
dynamic hint}) {
return ''; return '';
} }
void sessionTryAddDisplay(
{required UuidValue sessionId,
required Int32List displays,
dynamic hint}) {}
String sessionAddSync( String sessionAddSync(
{required UuidValue sessionId, {required UuidValue sessionId,
required String id, required String id,
@@ -94,6 +92,14 @@ class RustdeskImpl {
return Stream.empty(); return Stream.empty();
} }
Stream<EventToUI> sessionStartWithDisplays(
{required UuidValue sessionId,
required String id,
required Int32List displays,
dynamic hint}) {
throw UnimplementedError();
}
Future<bool?> sessionGetRemember( Future<bool?> sessionGetRemember(
{required UuidValue sessionId, dynamic hint}) { {required UuidValue sessionId, dynamic hint}) {
return Future( return Future(
@@ -203,12 +209,6 @@ class RustdeskImpl {
])); ]));
} }
Future<String?> sessionGetFlutterOptionByPeerId(
{required String id, required String k, dynamic hint}) {
return Future(
() => js.context.callMethod('getByName', ['option:flutter:peer', k]));
}
int getNextTextureKey({dynamic hint}) { int getNextTextureKey({dynamic hint}) {
return 0; return 0;
} }
@@ -346,6 +346,10 @@ class RustdeskImpl {
return mode == kKeyLegacyMode; return mode == kKeyLegacyMode;
} }
bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
return false;
}
Future<void> sessionSetCustomImageQuality( Future<void> sessionSetCustomImageQuality(
{required UuidValue sessionId, required int value, dynamic hint}) { {required UuidValue sessionId, required int value, dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [ return Future(() => js.context.callMethod('setByName', [
@@ -676,7 +680,8 @@ class RustdeskImpl {
return Future(() => js.context.callMethod('setByName', ['options', json])); return Future(() => js.context.callMethod('setByName', ['options', json]));
} }
Future<String> mainTestIfValidServer({required String server, dynamic hint}) { Future<String> mainTestIfValidServer(
{required String server, required bool testWithProxy, dynamic hint}) {
// TODO: implement // TODO: implement
return Future.value(''); return Future.value('');
} }
@@ -770,6 +775,24 @@ class RustdeskImpl {
throw UnimplementedError(); throw UnimplementedError();
} }
Future<bool> mainGetProxyStatus({dynamic hint}) {
return Future(() => false);
}
Future<void> mainHttpRequest({
required String url,
required String method,
String? body,
required String header,
dynamic hint,
}) {
throw UnimplementedError();
}
Future<String?> mainGetHttpStatus({required String url, dynamic hint}) {
throw UnimplementedError();
}
String mainGetLocalOption({required String key, dynamic hint}) { String mainGetLocalOption({required String key, dynamic hint}) {
return js.context.callMethod('getByName', ['option:local', key]); return js.context.callMethod('getByName', ['option:local', key]);
} }
@@ -920,7 +943,7 @@ class RustdeskImpl {
throw UnimplementedError(); throw UnimplementedError();
} }
Future<String> mainDefaultVideoSaveDirectory({dynamic hint}) { String mainVideoSaveDirectory({required bool root, dynamic hint}) {
throw UnimplementedError(); throw UnimplementedError();
} }
@@ -1052,7 +1075,7 @@ class RustdeskImpl {
throw UnimplementedError(); throw UnimplementedError();
} }
bool mainHasGpucodec({dynamic hint}) { bool mainHasVram({dynamic hint}) {
throw UnimplementedError(); throw UnimplementedError();
} }
@@ -1389,18 +1412,10 @@ class RustdeskImpl {
return false; return false;
} }
Future<void> mainStartPa({dynamic hint}) {
throw UnimplementedError();
}
bool mainHideDocker({dynamic hint}) { bool mainHideDocker({dynamic hint}) {
throw UnimplementedError(); throw UnimplementedError();
} }
bool mainHasPixelbufferTextureRender({dynamic hint}) {
return false;
}
bool mainHasFileClipboard({dynamic hint}) { bool mainHasFileClipboard({dynamic hint}) {
return false; return false;
} }
@@ -1447,6 +1462,10 @@ class RustdeskImpl {
return false; return false;
} }
bool isDisableGroupPanel({dynamic hint}) {
return false;
}
bool isDisableAccount({dynamic hint}) { bool isDisableAccount({dynamic hint}) {
return false; return false;
} }
@@ -1573,5 +1592,27 @@ class RustdeskImpl {
throw UnimplementedError(); throw UnimplementedError();
} }
Future<void> mainCheckHwcodec({dynamic hint}) {
throw UnimplementedError();
}
Future<void> sessionRequestNewDisplayInitMsgs(
{required UuidValue sessionId, required int display, dynamic hint}) {
throw UnimplementedError();
}
Future<String> mainHandleWaylandScreencastRestoreToken(
{required String key, required String value, dynamic hint}) {
throw UnimplementedError();
}
bool mainIsOptionFixed({required String key, dynamic hint}) {
throw UnimplementedError();
}
bool mainGetUseTextureRender({dynamic hint}) {
throw UnimplementedError();
}
void dispose() {} void dispose() {}
} }

View File

@@ -2,21 +2,33 @@
#include "my_application.h" #include "my_application.h"
#define RUSTDESK_LIB_PATH "librustdesk.so" #define RUSTDESK_LIB_PATH "librustdesk.so"
// #define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so"
typedef bool (*RustDeskCoreMain)(); typedef bool (*RustDeskCoreMain)();
bool gIsConnectionManager = false; bool gIsConnectionManager = false;
void print_help_install_pkg(const char* so);
bool flutter_rustdesk_core_main() { bool flutter_rustdesk_core_main() {
void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY); void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY);
if (!librustdesk) { if (!librustdesk) {
fprintf(stderr,"load librustdesk.so failed\n"); fprintf(stderr,"Failed to load \"librustdesk.so\"\n");
return true; char* error;
if ((error = dlerror()) != nullptr) {
fprintf(stderr, "%s\n", error);
char* libmissed = strstr(error, ": cannot open shared object file: No such file or directory");
if (libmissed != nullptr) {
*libmissed = '\0';
char* so = strdup(error);
print_help_install_pkg(so);
free(so);
}
}
return false;
} }
auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main"); auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main");
char* error; char* error;
if ((error = dlerror()) != nullptr) { if ((error = dlerror()) != nullptr) {
fprintf(stderr, "error finding rustdesk_core_main: %s", error); fprintf(stderr, "Program entry \"rustdesk_core_main\" is not found: %s\n", error);
return true; return false;
} }
return core_main(); return core_main();
} }
@@ -33,3 +45,80 @@ int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new(); g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv); return g_application_run(G_APPLICATION(app), argc, argv);
} }
typedef struct {
const char* mgr;
const char* search;
} PkgMgrSearch;
const PkgMgrSearch g_mgrs[] = {
{
"apt",
"apt-file search",
},
{
"dnf",
"dnf provides",
},
{
"yum",
"yum provides",
},
{
"zypper",
"zypper wp",
},
{
"pacman",
"pacman -Qo",
},
{
NULL,
NULL,
},
};
int is_command_exists(const char* command) {
char* path = getenv("PATH");
char* path_copy = strdup(path);
char* dir = strtok(path_copy, ":");
while (dir != NULL) {
char command_path[256];
snprintf(command_path, sizeof(command_path), "%s/%s", dir, command);
if (access(command_path, X_OK) == 0) {
free(path_copy);
return 1;
}
dir = strtok(NULL, ":");
}
free(path_copy);
return 0;
}
// We do not automatically search pkg
// as the search process can be time consuming and update may be required.
void print_help_install_pkg(const char* so)
{
if (strcmp(so, "libnsl.so.1") == 0) {
const char* mgr[] = {"yum", "dnf", NULL};
const char** m = mgr;
while (*m != NULL) {
if (is_command_exists(*m)) {
fprintf(stderr, "Please run \"%s install libnsl\" to install the required package.\n", *m);
return;
}
m++;
}
}
const PkgMgrSearch *mgr_search = g_mgrs;
while (mgr_search->mgr != NULL) {
if (is_command_exists(mgr_search->mgr) == 1) {
fprintf(stderr, "Please run \"%s %s\" to search and install the pkg.\n", mgr_search->search, so);
break;
}
mgr_search++;
}
}

View File

@@ -21,7 +21,6 @@ static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application); MyApplication* self = MY_APPLICATION(application);
GtkWindow* window = GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// we have custom window frame
gtk_window_set_decorated(window, FALSE); gtk_window_set_decorated(window, FALSE);
// try setting icon for rustdesk, which uses the system cache // try setting icon for rustdesk, which uses the system cache
GtkIconTheme* theme = gtk_icon_theme_get_default(); GtkIconTheme* theme = gtk_icon_theme_get_default();
@@ -75,12 +74,7 @@ static void my_application_activate(GApplication* application) {
FlView* view = fl_view_new(project); FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view)); gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
auto border_frame = gtk_frame_new(nullptr);
gtk_frame_set_shadow_type(GTK_FRAME(border_frame), GTK_SHADOW_ETCHED_IN);
gtk_container_add(GTK_CONTAINER(border_frame), GTK_WIDGET(view));
gtk_widget_show(GTK_WIDGET(border_frame));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(border_frame));
fl_register_plugins(FL_PLUGIN_REGISTRY(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view));

View File

@@ -210,7 +210,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 0920; LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1430; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
33CC10EC2044A3C60003C045 = { 33CC10EC2044A3C60003C045 = {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1510"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash #!/usr/bin/env bash
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter,hwcodec

View File

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

View File

@@ -335,7 +335,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: ef03db52a20a7899da135d694c071fa3866c8fb1 resolved-ref: "60773827434eefe6d01eefa814dca9a032b970b3"
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"
@@ -790,13 +790,13 @@ packages:
source: hosted source: hosted
version: "0.2.1+1" version: "0.2.1+1"
intl: intl:
dependency: transitive dependency: "direct overridden"
description: description:
name: intl name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.18.1" version: "0.19.0"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -849,18 +849,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.8.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.11.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -921,10 +921,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.3" version: "1.9.0"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
@@ -1526,10 +1526,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.0" version: "0.4.2"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:

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.4+39 version: 1.2.6+44
environment: environment:
sdk: '^3.1.0' sdk: '^3.1.0'
@@ -114,6 +114,9 @@ dev_dependencies:
flutter_lints: ^2.0.2 flutter_lints: ^2.0.2
ffigen: ^8.0.2 ffigen: ^8.0.2
dependency_overrides:
intl: ^0.19.0
# rerun: flutter pub run flutter_launcher_icons # rerun: flutter pub run flutter_launcher_icons
flutter_icons: flutter_icons:
image_path: "../res/icon.png" image_path: "../res/icon.png"

View File

@@ -5,5 +5,5 @@ flutter pub get
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
# call `flutter clean` if cargo build fails # call `flutter clean` if cargo build fails
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/ # export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
cargo build --features "flutter,flutter_texture_render" cargo build --features flutter
flutter run $@ flutter run $@

View File

@@ -1,17 +0,0 @@
# RustDesk web
## Functions
### Current and planned
- [x] Outgoing
- [ ] Address book
- [ ] Force relay
### Unsupported
1. Incoming
2. LAN
3. Gpu texture render. We use WebGL instead.
### No plans

View File

@@ -1 +0,0 @@
<svg viewBox="0 0 375 375" style="width:32px;height:32px;margin:0 4px 4px 0" xmlns="http://www.w3.org/2000/svg"><rect transform="matrix(.91553 0 0 .91553 -152.92 116.76)" x="167.03" y="-127.54" width="409.6" height="409.6" rx="64" ry="64" fill="#0071ff"></rect><path d="M150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

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