mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-01-19 00:13:01 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
e967294f1a
@ -10,7 +10,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/java:1": {},
|
||||
"ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": {
|
||||
"PACKAGES": "platform-tools,ndk;22.1.7171670"
|
||||
"PACKAGES": "platform-tools,ndk;23.2.8568313"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
@ -31,4 +31,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
75
.github/workflows/bridge.yml
vendored
Normal file
75
.github/workflows/bridge.yml
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
# This yaml shares the build bridge steps with ci and nightly.
|
||||
name: Build flutter-rust-bridge
|
||||
# 2023-04-19 15:48:00+00:00
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
generate_bridge:
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
sudo apt install ca-certificates -y
|
||||
sudo apt update -y
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: bridge-${{ matrix.job.os }}
|
||||
workspace: "/tmp/flutter_rust_bridge/frb_codegen"
|
||||
|
||||
- name: Cache Bridge
|
||||
id: cache-bridge
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/flutter_rust_bridge
|
||||
key: vcpkg-${{ matrix.job.arch }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen
|
||||
pushd flutter && flutter pub get && popd
|
||||
|
||||
- name: Run flutter rust bridge
|
||||
run: |
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: |
|
||||
./src/bridge_generated.rs
|
||||
./src/bridge_generated.io.rs
|
||||
./flutter/lib/generated_bridge.dart
|
||||
./flutter/lib/generated_bridge.freezed.dart
|
1709
.github/workflows/flutter-build.yml
vendored
Normal file
1709
.github/workflows/flutter-build.yml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
960
.github/workflows/flutter-ci.yml
vendored
960
.github/workflows/flutter-ci.yml
vendored
@ -16,959 +16,9 @@ on:
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
|
||||
env:
|
||||
LLVM_VERSION: "15.0.6"
|
||||
FLUTTER_VERSION: "3.7.5"
|
||||
# vcpkg version: 2022.05.10
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44"
|
||||
VERSION: "1.2.0"
|
||||
NDK_VERSION: "r23"
|
||||
|
||||
jobs:
|
||||
build-for-windows:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2019 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v1
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Replace engine with rustdesk custom flutter engine
|
||||
run: |
|
||||
flutter doctor -v
|
||||
flutter precache --windows
|
||||
Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip
|
||||
Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
|
||||
mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
|
||||
|
||||
- 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)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen
|
||||
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: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
shell: bash
|
||||
|
||||
- name: Build rustdesk
|
||||
run: python3 .\build.py --portable --hwcodec --flutter
|
||||
|
||||
build-for-macOS:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install build runtime
|
||||
run: |
|
||||
brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen
|
||||
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
|
||||
|
||||
- name: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx libyuv opus
|
||||
|
||||
- name: Show version information (Rust, cargo, Clang)
|
||||
shell: bash
|
||||
run: |
|
||||
clang --version || true
|
||||
rustup -V
|
||||
rustup toolchain list
|
||||
rustup default
|
||||
cargo -V
|
||||
rustc -V
|
||||
|
||||
- name: Build rustdesk
|
||||
run: |
|
||||
# --hwcodec not supported on macos yet
|
||||
./build.py --flutter ${{ matrix.job.extra-build-args }}
|
||||
|
||||
build-vcpkg-deps-linux:
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
# - { arch: armv7, os: ubuntu-20.04 }
|
||||
- { arch: x86_64, os: ubuntu-20.04 }
|
||||
- { arch: aarch64, os: ubuntu-20.04 }
|
||||
steps:
|
||||
- name: Create vcpkg artifacts folder
|
||||
run: mkdir -p /opt/artifacts
|
||||
|
||||
- name: Cache Vcpkg
|
||||
id: cache-vcpkg
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/artifacts
|
||||
key: vcpkg-${{ matrix.job.arch }}
|
||||
|
||||
- uses: Kingtous/run-on-arch-action@amd64-support
|
||||
name: Run vcpkg install on ${{ matrix.job.arch }}
|
||||
id: vcpkg
|
||||
with:
|
||||
arch: ${{ matrix.job.arch }}
|
||||
distro: ubuntu18.04
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "/opt/artifacts"
|
||||
dockerRunArgs: |
|
||||
--volume "/opt/artifacts:/artifacts"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt update -y
|
||||
case "${{ matrix.job.arch }}" in
|
||||
x86_64)
|
||||
# CMake 3.15+
|
||||
apt install -y gpg wget ca-certificates
|
||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null
|
||||
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
||||
apt update -y
|
||||
apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev
|
||||
;;
|
||||
aarch64|armv7)
|
||||
apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool
|
||||
esac
|
||||
cmake --version
|
||||
gcc -v
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
case "${{ matrix.job.arch }}" in
|
||||
x86_64)
|
||||
export VCPKG_FORCE_SYSTEM_BINARIES=1
|
||||
pushd /artifacts
|
||||
git clone https://github.com/microsoft/vcpkg.git || true
|
||||
pushd vcpkg
|
||||
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
|
||||
./bootstrap-vcpkg.sh
|
||||
./vcpkg install libvpx libyuv opus
|
||||
;;
|
||||
aarch64|armv7)
|
||||
pushd /artifacts
|
||||
# libyuv
|
||||
git clone https://chromium.googlesource.com/libyuv/libyuv || true
|
||||
pushd libyuv
|
||||
git pull
|
||||
mkdir -p build
|
||||
pushd build
|
||||
mkdir -p /artifacts/vcpkg/installed
|
||||
cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed
|
||||
make -j4 && make install
|
||||
popd
|
||||
popd
|
||||
# libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC
|
||||
wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz
|
||||
tar -zxvf opus.tar.gz; ls -l
|
||||
pushd opus-1.1.2
|
||||
./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed
|
||||
make -j4; make install
|
||||
;;
|
||||
esac
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: vcpkg-artifact-${{ matrix.job.arch }}
|
||||
path: |
|
||||
/opt/artifacts/vcpkg/installed
|
||||
|
||||
generate-bridge-linux:
|
||||
name: generate bridge
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
sudo apt update -y
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: bridge-${{ matrix.job.os }}
|
||||
workspace: "/tmp/flutter_rust_bridge/frb_codegen"
|
||||
|
||||
- name: Cache Bridge
|
||||
id: cache-bridge
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/flutter_rust_bridge
|
||||
key: vcpkg-${{ matrix.job.arch }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install flutter_rust_bridge_codegen
|
||||
pushd flutter && flutter pub get && popd
|
||||
|
||||
- name: Run flutter rust bridge
|
||||
run: |
|
||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: |
|
||||
./src/bridge_generated.rs
|
||||
./src/bridge_generated.io.rs
|
||||
./flutter/lib/generated_bridge.dart
|
||||
./flutter/lib/generated_bridge.freezed.dart
|
||||
|
||||
build-rustdesk-android:
|
||||
needs: [generate-bridge-linux]
|
||||
name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-features: "",
|
||||
openssl-arch: android-arm64
|
||||
}
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: armv7-linux-androideabi,
|
||||
os: ubuntu-18.04,
|
||||
extra-build-features: "",
|
||||
openssl-arch: android-arm
|
||||
}
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: ${{ env.NDK_VERSION }}
|
||||
add-to-path: true
|
||||
|
||||
- name: Clone deps
|
||||
shell: bash
|
||||
run: |
|
||||
pushd /opt
|
||||
git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: rustdesk-lib-cache
|
||||
key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
|
||||
|
||||
- name: Disable rust bridge build
|
||||
run: |
|
||||
sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs
|
||||
|
||||
- name: Build rustdesk lib
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg
|
||||
run: |
|
||||
rustup target add ${{ matrix.job.target }}
|
||||
cargo install cargo-ndk
|
||||
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 /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/arm64-v8a/*.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.target }}-release.apk
|
||||
;;
|
||||
armv7-linux-androideabi)
|
||||
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||
cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/armeabi-v7a/*.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.target }}-release.apk
|
||||
;;
|
||||
esac
|
||||
popd
|
||||
mkdir -p signed-apk; pushd signed-apk
|
||||
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk .
|
||||
|
||||
build-rustdesk-lib-linux-amd64:
|
||||
needs: [generate-bridge-linux, build-vcpkg-deps-linux]
|
||||
name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
# use a high level qemu-user-static
|
||||
job:
|
||||
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-features: "",
|
||||
}
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-features: "flatpak",
|
||||
}
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-features: "appimage",
|
||||
}
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
run: |
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo apt update -y
|
||||
sudo apt install qemu-user-static
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 12
|
||||
|
||||
- name: Free Space
|
||||
run: |
|
||||
df
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: rustdesk-lib-cache
|
||||
key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
|
||||
cache-directories: "/opt/rust-registry"
|
||||
|
||||
- name: Install local registry
|
||||
run: |
|
||||
mkdir -p /opt/rust-registry
|
||||
cargo install cargo-local-registry
|
||||
|
||||
- name: Build local registry
|
||||
uses: nick-fields/retry@v2
|
||||
id: build-local-registry
|
||||
continue-on-error: true
|
||||
with:
|
||||
max_attempts: 3
|
||||
timeout_minutes: 15
|
||||
retry_on: error
|
||||
command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry
|
||||
|
||||
- name: Disable rust bridge build
|
||||
run: |
|
||||
sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs
|
||||
# only build cdylib
|
||||
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Restore vcpkg files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: vcpkg-artifact-${{ matrix.job.arch }}
|
||||
path: /opt/artifacts/vcpkg/installed
|
||||
|
||||
- uses: Kingtous/run-on-arch-action@amd64-support
|
||||
name: Build rustdesk library for ${{ matrix.job.arch }}
|
||||
id: vcpkg
|
||||
with:
|
||||
arch: ${{ matrix.job.arch }}
|
||||
distro: ubuntu18.04
|
||||
# not ready yet
|
||||
# distro: ubuntu18.04-rustdesk
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "${PWD}"
|
||||
ls -l /opt/artifacts/vcpkg/installed
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}:/workspace"
|
||||
--volume "/opt/artifacts:/opt/artifacts"
|
||||
--volume "/opt/rust-registry:/opt/rust-registry"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt update -y
|
||||
echo -e "installing deps"
|
||||
apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null
|
||||
# we have libopus compiled by us.
|
||||
apt remove -y libopus-dev || true
|
||||
# output devs
|
||||
ls -l ./
|
||||
tree -L 3 /opt/artifacts/vcpkg/installed
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
# rust
|
||||
pushd /opt
|
||||
wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz
|
||||
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
|
||||
cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh
|
||||
rm -rf rust-1.64.0-${{ matrix.job.target }}
|
||||
# edit config
|
||||
mkdir -p ~/.cargo/
|
||||
echo """
|
||||
[source.crates-io]
|
||||
registry = 'https://github.com/rust-lang/crates.io-index'
|
||||
replace-with = 'local-registry'
|
||||
|
||||
[source.local-registry]
|
||||
local-registry = '/opt/rust-registry/'
|
||||
""" > ~/.cargo/config
|
||||
cat ~/.cargo/config
|
||||
# start build
|
||||
pushd /workspace
|
||||
# mock
|
||||
case "${{ matrix.job.arch }}" in
|
||||
x86_64)
|
||||
# no need mock on x86_64
|
||||
export VCPKG_ROOT=/opt/artifacts/vcpkg
|
||||
cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so
|
||||
path: target/release/liblibrustdesk.so
|
||||
|
||||
build-rustdesk-lib-linux-arm:
|
||||
needs: [generate-bridge-linux, build-vcpkg-deps-linux]
|
||||
name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
# use a high level qemu-user-static
|
||||
job:
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
use-cross: true,
|
||||
extra-build-features: "",
|
||||
}
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
os: ubuntu-18.04, # just for naming package, not running host
|
||||
use-cross: true,
|
||||
extra-build-features: "appimage",
|
||||
}
|
||||
# - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
|
||||
# - {
|
||||
# arch: armv7,
|
||||
# target: arm-unknown-linux-gnueabihf,
|
||||
# os: ubuntu-20.04,
|
||||
# use-cross: true,
|
||||
# extra-build-features: "",
|
||||
# }
|
||||
# - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
|
||||
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
run: |
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo apt update -y
|
||||
sudo apt install qemu-user-static
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 12
|
||||
|
||||
- name: Free Space
|
||||
run: |
|
||||
df
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: rustdesk-lib-cache
|
||||
key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
|
||||
cache-directories: "/opt/rust-registry"
|
||||
|
||||
- name: Install local registry
|
||||
run: |
|
||||
mkdir -p /opt/rust-registry
|
||||
cargo install cargo-local-registry
|
||||
|
||||
- name: Build local registry
|
||||
uses: nick-fields/retry@v2
|
||||
id: build-local-registry
|
||||
continue-on-error: true
|
||||
with:
|
||||
max_attempts: 3
|
||||
timeout_minutes: 15
|
||||
retry_on: error
|
||||
command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry
|
||||
|
||||
- name: Disable rust bridge build
|
||||
run: |
|
||||
sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs
|
||||
# only build cdylib
|
||||
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Restore vcpkg files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: vcpkg-artifact-${{ matrix.job.arch }}
|
||||
path: /opt/artifacts/vcpkg/installed
|
||||
|
||||
- uses: Kingtous/run-on-arch-action@amd64-support
|
||||
name: Build rustdesk library for ${{ matrix.job.arch }}
|
||||
id: vcpkg
|
||||
with:
|
||||
arch: ${{ matrix.job.arch }}
|
||||
distro: ubuntu18.04-rustdesk
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "${PWD}"
|
||||
ls -l /opt/artifacts/vcpkg/installed
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}:/workspace"
|
||||
--volume "/opt/artifacts:/opt/artifacts"
|
||||
--volume "/opt/rust-registry:/opt/rust-registry"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt update -y
|
||||
echo -e "installing deps"
|
||||
apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null
|
||||
# we have libopus compiled by us.
|
||||
apt remove -y libopus-dev || true
|
||||
# output devs
|
||||
ls -l ./
|
||||
tree -L 3 /opt/artifacts/vcpkg/installed
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
# rust
|
||||
pushd /opt
|
||||
wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz
|
||||
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
|
||||
cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh
|
||||
rm -rf rust-1.64.0-${{ matrix.job.target }}
|
||||
# edit config
|
||||
mkdir -p ~/.cargo/
|
||||
echo """
|
||||
[source.crates-io]
|
||||
registry = 'https://github.com/rust-lang/crates.io-index'
|
||||
replace-with = 'local-registry'
|
||||
|
||||
[source.local-registry]
|
||||
local-registry = '/opt/rust-registry/'
|
||||
""" > ~/.cargo/config
|
||||
cat ~/.cargo/config
|
||||
# start build
|
||||
pushd /workspace
|
||||
# mock
|
||||
case "${{ matrix.job.arch }}" in
|
||||
aarch64)
|
||||
cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/
|
||||
cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/
|
||||
ls -l /opt/artifacts/vcpkg/installed/lib/
|
||||
mkdir -p /vcpkg/installed/arm64-linux
|
||||
ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib
|
||||
ln -s /usr/include /vcpkg/installed/arm64-linux/include
|
||||
export VCPKG_ROOT=/vcpkg
|
||||
# disable hwcodec for compilation
|
||||
cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
|
||||
;;
|
||||
armv7)
|
||||
cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/
|
||||
cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/
|
||||
mkdir -p /vcpkg/installed/arm-linux
|
||||
ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib
|
||||
ln -s /usr/include /vcpkg/installed/arm-linux/include
|
||||
export VCPKG_ROOT=/vcpkg
|
||||
# disable hwcodec for compilation
|
||||
cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so
|
||||
path: target/release/liblibrustdesk.so
|
||||
|
||||
build-rustdesk-linux-arm:
|
||||
needs: [build-rustdesk-lib-linux-arm]
|
||||
name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
|
||||
runs-on: ubuntu-20.04 # 20.04 has more performance on arm build
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
os: ubuntu-18.04, # just for naming package, not running host
|
||||
use-cross: true,
|
||||
extra-build-features: "",
|
||||
}
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
os: ubuntu-18.04, # just for naming package, not running host
|
||||
use-cross: true,
|
||||
extra-build-features: "appimage",
|
||||
}
|
||||
# - {
|
||||
# arch: aarch64,
|
||||
# target: aarch64-unknown-linux-gnu,
|
||||
# os: ubuntu-18.04, # just for naming package, not running host
|
||||
# use-cross: true,
|
||||
# extra-build-features: "flatpak",
|
||||
# }
|
||||
# - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" }
|
||||
# - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
|
||||
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Prepare env
|
||||
run: |
|
||||
sudo apt update -y
|
||||
sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools
|
||||
mkdir -p ./target/release/
|
||||
|
||||
- name: Restore the rustdesk lib file
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so
|
||||
path: ./target/release/
|
||||
|
||||
- name: Download Flutter
|
||||
shell: bash
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
pushd /opt
|
||||
# clone repo and reset to flutter 3.7.0
|
||||
git clone https://github.com/sony/flutter-elinux.git || true
|
||||
pushd flutter-elinux
|
||||
# reset to flutter 3.7.0
|
||||
git fetch
|
||||
git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5
|
||||
popd
|
||||
|
||||
- uses: Kingtous/run-on-arch-action@amd64-support
|
||||
name: Build rustdesk binary for ${{ matrix.job.arch }}
|
||||
id: vcpkg
|
||||
with:
|
||||
arch: ${{ matrix.job.arch }}
|
||||
distro: ubuntu18.04-rustdesk
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "${PWD}"
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}:/workspace"
|
||||
--volume "/opt/artifacts:/opt/artifacts"
|
||||
--volume "/opt/flutter-elinux:/opt/flutter-elinux"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt update -y
|
||||
apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
pushd /workspace
|
||||
# we use flutter-elinux to build our rustdesk
|
||||
export PATH=/opt/flutter-elinux/bin:$PATH
|
||||
sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py
|
||||
# Setup flutter-elinux. Run doctor to check if issues here.
|
||||
flutter-elinux doctor -v
|
||||
# Patch arm64 engine for flutter 3.6.0+
|
||||
flutter-elinux precache --linux
|
||||
pushd /tmp
|
||||
curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz
|
||||
tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib
|
||||
cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64
|
||||
popd
|
||||
case ${{ matrix.job.arch }} in
|
||||
aarch64)
|
||||
sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py
|
||||
sed -i "s/x64\/release/arm64\/release/g" ./build.py
|
||||
;;
|
||||
armv7)
|
||||
sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py
|
||||
sed -i "s/x64\/release/arm\/release/g" ./build.py
|
||||
;;
|
||||
esac
|
||||
python3 ./build.py --flutter --hwcodec --skip-cargo
|
||||
|
||||
build-rustdesk-linux-amd64:
|
||||
needs: [build-rustdesk-lib-linux-amd64]
|
||||
name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-features: "",
|
||||
}
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-features: "flatpak",
|
||||
}
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-features: "appimage",
|
||||
}
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Prepare env
|
||||
run: |
|
||||
sudo apt update -y
|
||||
sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools
|
||||
mkdir -p ./target/release/
|
||||
|
||||
- name: Restore the rustdesk lib file
|
||||
uses: actions/download-artifact@master
|
||||
with:
|
||||
name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so
|
||||
path: ./target/release/
|
||||
|
||||
- uses: Kingtous/run-on-arch-action@amd64-support
|
||||
name: Build rustdesk binary for ${{ matrix.job.arch }}
|
||||
id: vcpkg
|
||||
with:
|
||||
arch: ${{ matrix.job.arch }}
|
||||
distro: ubuntu18.04
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "${PWD}"
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}:/workspace"
|
||||
--volume "/opt/artifacts:/opt/artifacts"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt update -y
|
||||
apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
# Setup Flutter
|
||||
pushd /opt
|
||||
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
|
||||
tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
|
||||
ls -l .
|
||||
export PATH=/opt/flutter/bin:$PATH
|
||||
flutter doctor -v
|
||||
pushd /workspace
|
||||
python3 ./build.py --flutter --hwcodec --skip-cargo
|
||||
run-ci:
|
||||
uses: ./.github/workflows/flutter-build.yml
|
||||
with:
|
||||
upload-artifact: false
|
||||
|
1517
.github/workflows/flutter-nightly.yml
vendored
1517
.github/workflows/flutter-nightly.yml
vendored
File diff suppressed because it is too large
Load Diff
370
.github/workflows/history.yml
vendored
Normal file
370
.github/workflows/history.yml
vendored
Normal file
@ -0,0 +1,370 @@
|
||||
name: Flutter Windows History Build
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
env:
|
||||
LLVM_VERSION: "10.0"
|
||||
# Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first.
|
||||
FLUTTER_VERSION: "3.0.5"
|
||||
TAG_NAME: "tmp"
|
||||
# vcpkg version: 2022.05.10
|
||||
# for multiarch gcc compatibility
|
||||
VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44"
|
||||
VERSION: "1.2.0"
|
||||
|
||||
jobs:
|
||||
build-for-windows-2022-12-05:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2019 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: '8d1254cf14b69f545c9cefa026c5eeb0e7dd3e7c'
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v1
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Replace engine with rustdesk custom flutter engine
|
||||
run: |
|
||||
flutter doctor -v
|
||||
flutter precache --windows
|
||||
Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
|
||||
Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
|
||||
mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.62"
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
run: |
|
||||
dart pub global activate ffigen --version 5.0.1
|
||||
$exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
|
||||
Push-Location ..
|
||||
git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
|
||||
Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
|
||||
Pop-Location
|
||||
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: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
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-2022-12-05-${{ 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
|
||||
|
||||
build-for-windows-2022-12-12:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2019 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: '3dd43b79ec0409fc38103bed0c7eb0bc3cd993d5'
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v1
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Replace engine with rustdesk custom flutter engine
|
||||
run: |
|
||||
flutter doctor -v
|
||||
flutter precache --windows
|
||||
Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
|
||||
Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
|
||||
mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.62"
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
run: |
|
||||
dart pub global activate ffigen --version 5.0.1
|
||||
$exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
|
||||
Push-Location ..
|
||||
git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
|
||||
Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
|
||||
Pop-Location
|
||||
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: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
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-2022-12-12-${{ 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
|
||||
|
||||
build-for-windows-2022-12-19:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2019 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: '1054715891c4e73ad9b164acec6dadecfc599a65'
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v1
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Replace engine with rustdesk custom flutter engine
|
||||
run: |
|
||||
flutter doctor -v
|
||||
flutter precache --windows
|
||||
Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
|
||||
Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
|
||||
mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.62"
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
run: |
|
||||
dart pub global activate ffigen --version 5.0.1
|
||||
$exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
|
||||
Push-Location ..
|
||||
git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
|
||||
Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
|
||||
Pop-Location
|
||||
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: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
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-2022-12-19-${{ 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
|
||||
|
||||
build-for-windows-2022-12-26:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
||||
- { target: x86_64-pc-windows-msvc, os: windows-2019 }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'b241925fe093dc4da804a5aac419375f4ca7653f'
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: KyleMayes/install-llvm-action@v1
|
||||
with:
|
||||
version: ${{ env.LLVM_VERSION }}
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Replace engine with rustdesk custom flutter engine
|
||||
run: |
|
||||
flutter doctor -v
|
||||
flutter precache --windows
|
||||
Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
|
||||
Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
|
||||
mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.62"
|
||||
target: ${{ matrix.job.target }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.job.os }}
|
||||
|
||||
- name: Install flutter rust bridge deps
|
||||
run: |
|
||||
dart pub global activate ffigen --version 5.0.1
|
||||
$exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
|
||||
Push-Location ..
|
||||
git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
|
||||
Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
|
||||
Pop-Location
|
||||
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: Restore from cache and install vcpkg
|
||||
uses: lukka/run-vcpkg@v7
|
||||
with:
|
||||
setupOnly: true
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
$VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
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-2022-12-26-${{ 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
|
88
.github/workflows/vcpkg-deps-linux.yml
vendored
Normal file
88
.github/workflows/vcpkg-deps-linux.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
name: Build vcpkg dependencies for linux clients
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build-vcpkg-deps-linux:
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
job:
|
||||
- { arch: armv7, os: ubuntu-20.04 }
|
||||
- { arch: x86_64, os: ubuntu-20.04 }
|
||||
- { arch: aarch64, os: ubuntu-20.04 }
|
||||
steps:
|
||||
- name: Create vcpkg artifacts folder
|
||||
run: mkdir -p /opt/artifacts
|
||||
|
||||
- name: Cache Vcpkg
|
||||
id: cache-vcpkg
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/artifacts
|
||||
key: vcpkg-${{ matrix.job.arch }}
|
||||
|
||||
- uses: Kingtous/run-on-arch-action@amd64-support
|
||||
name: Run vcpkg install on ${{ matrix.job.arch }}
|
||||
id: vcpkg
|
||||
with:
|
||||
arch: ${{ matrix.job.arch }}
|
||||
distro: ubuntu18.04
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "/opt/artifacts"
|
||||
dockerRunArgs: |
|
||||
--volume "/opt/artifacts:/artifacts"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt update -y
|
||||
case "${{ matrix.job.arch }}" in
|
||||
x86_64)
|
||||
# CMake 3.15+
|
||||
apt install -y gpg wget ca-certificates
|
||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null
|
||||
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
||||
apt update -y
|
||||
apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev
|
||||
cmake --version
|
||||
gcc -v
|
||||
;;
|
||||
aarch64|armv7)
|
||||
apt install -y curl zip unzip git
|
||||
esac
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
case "${{ matrix.job.arch }}" in
|
||||
x86_64)
|
||||
export VCPKG_FORCE_SYSTEM_BINARIES=1
|
||||
pushd /artifacts
|
||||
git clone https://github.com/microsoft/vcpkg.git || true
|
||||
pushd vcpkg
|
||||
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
|
||||
./bootstrap-vcpkg.sh
|
||||
./vcpkg install libvpx libyuv opus
|
||||
;;
|
||||
aarch64)
|
||||
pushd /artifacts
|
||||
rm -rf rustdesk_thirdparty_lib
|
||||
git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1
|
||||
mkdir -p /artifacts/vcpkg/installed
|
||||
mv ./rustdesk_thirdparty_lib/vcpkg/installed/arm64-linux /artifacts/vcpkg/installed/arm64-linux
|
||||
;;
|
||||
armv7)
|
||||
pushd /artifacts
|
||||
rm -rf rustdesk_thirdparty_lib
|
||||
git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1
|
||||
mkdir -p /artifacts/vcpkg/installed
|
||||
mv ./rustdesk_thirdparty_lib/vcpkg/installed/arm-linux /artifacts/vcpkg/installed/arm-linux
|
||||
;;
|
||||
esac
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: vcpkg-artifact-${{ matrix.job.arch }}
|
||||
path: |
|
||||
/opt/artifacts/vcpkg/installed
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -27,6 +27,7 @@ rustdesk
|
||||
appimage/AppDir
|
||||
appimage/*.AppImage
|
||||
appimage/appimage-build
|
||||
appimage/*.xz
|
||||
# flutter
|
||||
flutter/linux/build/**
|
||||
flutter/linux/cmake-build-debug/**
|
||||
@ -38,6 +39,8 @@ flatpak/.flatpak-builder/shared-modules/**
|
||||
flatpak/.flatpak-builder/shared-modules/*.tar.xz
|
||||
flatpak/.flatpak-builder/debian-binary
|
||||
flatpak/build/**
|
||||
flatpak/repo/**
|
||||
flatpak/*.flatpak
|
||||
# bridge file
|
||||
lib/generated_bridge.dart
|
||||
# vscode devcontainer
|
||||
@ -45,3 +48,5 @@ lib/generated_bridge.dart
|
||||
.vscode-server/
|
||||
.ssh
|
||||
.devcontainer/.*
|
||||
# build cache in examples
|
||||
examples/**/target/
|
1414
Cargo.lock
generated
1414
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@ -19,7 +19,6 @@ path = "src/naming.rs"
|
||||
inline = []
|
||||
hbbs = []
|
||||
cli = []
|
||||
with_rc = ["simple_rc"]
|
||||
flutter_texture_render = []
|
||||
appimage = []
|
||||
flatpak = []
|
||||
@ -30,6 +29,9 @@ flutter = ["flutter_rust_bridge"]
|
||||
default = ["use_dasp"]
|
||||
hwcodec = ["scrap/hwcodec"]
|
||||
mediacodec = ["scrap/mediacodec"]
|
||||
linux_headless = ["pam", "users"]
|
||||
virtual_display_driver = ["virtual_display"]
|
||||
plugin_framework = []
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -57,21 +59,22 @@ rpassword = "7.0"
|
||||
base64 = "0.21"
|
||||
num_cpus = "1.13"
|
||||
bytes = { version = "1.2", features = ["serde"] }
|
||||
default-net = "0.12.0"
|
||||
default-net = "0.14"
|
||||
wol-rs = "1.0"
|
||||
flutter_rust_bridge = { version = "1.61.1", optional = true }
|
||||
errno = "0.3"
|
||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
||||
url = { version = "2.1", features = ["serde"] }
|
||||
dlopen = "0.1"
|
||||
hex = "0.4.3"
|
||||
|
||||
crossbeam-queue = "0.3"
|
||||
hex = "0.4"
|
||||
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false }
|
||||
chrono = "0.4.23"
|
||||
cidr-utils = "0.5.9"
|
||||
chrono = "0.4"
|
||||
cidr-utils = "0.5"
|
||||
libloading = "0.7"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
|
||||
cpal = "0.14"
|
||||
cpal = "0.15"
|
||||
ringbuf = "0.3"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
machine-uid = "0.2"
|
||||
@ -91,10 +94,10 @@ winit = "0.26"
|
||||
winapi = { version = "0.3", features = ["winuser", "wincrypt"] }
|
||||
winreg = "0.10"
|
||||
windows-service = "0.4"
|
||||
virtual_display = { path = "libs/virtual_display" }
|
||||
virtual_display = { path = "libs/virtual_display", optional = true }
|
||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
||||
shared_memory = "0.12.4"
|
||||
shutdown_hooks = "0.1.0"
|
||||
shared_memory = "0.12"
|
||||
shutdown_hooks = "0.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2"
|
||||
@ -102,10 +105,10 @@ cocoa = "0.24"
|
||||
dispatch = "0.2"
|
||||
core-foundation = "0.9"
|
||||
core-graphics = "0.22"
|
||||
include_dir = "0.7.2"
|
||||
include_dir = "0.7"
|
||||
dark-light = "1.0"
|
||||
fruitbasket = "0.10.0"
|
||||
objc_id = "0.1.1"
|
||||
fruitbasket = "0.10"
|
||||
objc_id = "0.1"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
|
||||
tray-icon = "0.4"
|
||||
@ -121,7 +124,8 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
|
||||
evdev = { git="https://github.com/fufesou/evdev" }
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
xrandr-parser = "0.3.0"
|
||||
pam = { git="https://github.com/fufesou/pam", optional = true }
|
||||
users = { version = "0.11.0", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.11"
|
||||
@ -131,8 +135,8 @@ jni = "0.19"
|
||||
flutter_rust_bridge = "1.61.1"
|
||||
|
||||
[workspace]
|
||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"]
|
||||
exclude = ["vdi/host"]
|
||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"]
|
||||
exclude = ["vdi/host", "examples/custom_plugin"]
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2022 Purslane, Inc."
|
||||
@ -146,7 +150,6 @@ winapi = { version = "0.3", features = [ "winnt" ] }
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
simple_rc = { path = "libs/simple_rc", optional = true }
|
||||
flutter_rust_bridge_codegen = "1.61.1"
|
||||
os-version = "0.2"
|
||||
|
||||
@ -157,7 +160,6 @@ hound = "3.5"
|
||||
name = "RustDesk"
|
||||
identifier = "com.carriez.rustdesk"
|
||||
icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"]
|
||||
deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libvdpau1", "libva2"]
|
||||
osx_minimum_system_version = "10.14"
|
||||
|
||||
#https://github.com/johnthagen/min-sized-rust
|
||||
|
@ -32,6 +32,9 @@ AppDir:
|
||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted
|
||||
universe multiverse
|
||||
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
|
||||
universe multiverse
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||
include:
|
||||
- libc6
|
||||
- libgtk-3-0
|
||||
@ -47,6 +50,9 @@ AppDir:
|
||||
- libva-x11-2
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- libwayland-cursor0
|
||||
- libwayland-egl1
|
||||
- libpulse0
|
||||
exclude:
|
||||
- humanity-icon-theme
|
||||
- hicolor-icon-theme
|
||||
|
@ -33,6 +33,8 @@ AppDir:
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted
|
||||
universe multiverse
|
||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted
|
||||
universe multiverse
|
||||
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
|
||||
bionic main
|
||||
include:
|
||||
@ -50,6 +52,9 @@ AppDir:
|
||||
- libva-x11-2
|
||||
- libvdpau1
|
||||
- libgstreamer-plugins-base1.0-0
|
||||
- libwayland-cursor0
|
||||
- libwayland-egl1
|
||||
- libpulse0
|
||||
exclude:
|
||||
- humanity-icon-theme
|
||||
- hicolor-icon-theme
|
||||
|
152
build.py
152
build.py
@ -15,9 +15,21 @@ osx = platform.platform().startswith(
|
||||
'Darwin') or platform.platform().startswith("macOS")
|
||||
hbb_name = 'rustdesk' + ('.exe' if windows else '')
|
||||
exe_path = 'target/release/' + hbb_name
|
||||
flutter_win_target_dir = 'flutter/build/windows/runner/Release/'
|
||||
if windows:
|
||||
flutter_build_dir = 'build/windows/runner/Release/'
|
||||
elif osx:
|
||||
flutter_build_dir = 'build/macos/Build/Products/Release/'
|
||||
else:
|
||||
flutter_build_dir = 'build/linux/x64/release/bundle/'
|
||||
flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
|
||||
skip_cargo = False
|
||||
|
||||
def get_arch() -> str:
|
||||
custom_arch = os.environ.get("ARCH")
|
||||
if custom_arch is None:
|
||||
return "amd64"
|
||||
return custom_arch
|
||||
|
||||
def system2(cmd):
|
||||
err = os.system(cmd)
|
||||
if err != 0:
|
||||
@ -35,11 +47,13 @@ def get_version():
|
||||
def parse_rc_features(feature):
|
||||
available_features = {
|
||||
'IddDriver': {
|
||||
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip',
|
||||
'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/checksum_md5',
|
||||
'platform': ['windows'],
|
||||
'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.3/RustDeskIddDriver_x64.zip',
|
||||
'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.3/checksum_md5',
|
||||
'exclude': ['README.md', 'certmgr.exe', 'install_cert_runas_admin.bat'],
|
||||
},
|
||||
'PrivacyMode': {
|
||||
'platform': ['windows'],
|
||||
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1'
|
||||
'/TempTopMostWindow_x64_pic_en.zip',
|
||||
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5',
|
||||
@ -49,16 +63,34 @@ def parse_rc_features(feature):
|
||||
apply_features = {}
|
||||
if not feature:
|
||||
feature = []
|
||||
|
||||
def platform_check(platforms):
|
||||
if windows:
|
||||
return 'windows' in platforms
|
||||
elif osx:
|
||||
return 'osx' in platforms
|
||||
else:
|
||||
return 'linux' in platforms
|
||||
|
||||
def get_all_features():
|
||||
features = []
|
||||
for (feat, feat_info) in available_features.items():
|
||||
if platform_check(feat_info['platform']):
|
||||
features.append(feat)
|
||||
return features
|
||||
|
||||
if isinstance(feature, str) and feature.upper() == 'ALL':
|
||||
return available_features
|
||||
return get_all_features()
|
||||
elif isinstance(feature, list):
|
||||
# force add PrivacyMode
|
||||
feature.append('PrivacyMode')
|
||||
if windows:
|
||||
# force add PrivacyMode
|
||||
feature.append('PrivacyMode')
|
||||
for feat in feature:
|
||||
if isinstance(feat, str) and feat.upper() == 'ALL':
|
||||
return available_features
|
||||
return get_all_features()
|
||||
if feat in available_features:
|
||||
apply_features[feat] = available_features[feat]
|
||||
if platform_check(available_features[feat]['platform']):
|
||||
apply_features[feat] = available_features[feat]
|
||||
else:
|
||||
print(f'Unrecognized feature {feat}')
|
||||
return apply_features
|
||||
@ -106,6 +138,10 @@ def make_parser():
|
||||
action='store_true',
|
||||
help='Skip cargo build process, only flutter version + Linux supported currently'
|
||||
)
|
||||
parser.add_argument(
|
||||
"--package",
|
||||
type=str
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@ -201,14 +237,12 @@ def download_extract_features(features, res_dir):
|
||||
print(f'{feat} extract end')
|
||||
|
||||
|
||||
def get_rc_features(args):
|
||||
flutter = args.flutter
|
||||
def external_resources(flutter, args, res_dir):
|
||||
features = parse_rc_features(args.feature)
|
||||
if not features:
|
||||
return []
|
||||
return
|
||||
|
||||
print(f'Build with features {list(features.keys())}')
|
||||
res_dir = 'resources'
|
||||
if os.path.isdir(res_dir) and not os.path.islink(res_dir):
|
||||
shutil.rmtree(res_dir)
|
||||
elif os.path.exists(res_dir):
|
||||
@ -216,22 +250,19 @@ def get_rc_features(args):
|
||||
os.makedirs(res_dir, exist_ok=True)
|
||||
download_extract_features(features, res_dir)
|
||||
if flutter:
|
||||
os.makedirs(flutter_win_target_dir, exist_ok=True)
|
||||
os.makedirs(flutter_build_dir_2, exist_ok=True)
|
||||
for f in pathlib.Path(res_dir).iterdir():
|
||||
print(f'{f}')
|
||||
if f.is_file():
|
||||
shutil.copy2(f, flutter_win_target_dir)
|
||||
shutil.copy2(f, flutter_build_dir_2)
|
||||
else:
|
||||
shutil.copytree(f, f'{flutter_win_target_dir}{f.stem}')
|
||||
return []
|
||||
else:
|
||||
return ['with_rc']
|
||||
shutil.copytree(f, f'{flutter_build_dir_2}{f.stem}')
|
||||
|
||||
|
||||
def get_features(args):
|
||||
features = ['inline'] if not args.flutter else []
|
||||
if windows:
|
||||
features.extend(get_rc_features(args))
|
||||
features.append('virtual_display_driver')
|
||||
if args.hwcodec:
|
||||
features.append('hwcodec')
|
||||
if args.flutter:
|
||||
@ -251,13 +282,13 @@ def generate_control_file(version):
|
||||
|
||||
content = """Package: rustdesk
|
||||
Version: %s
|
||||
Architecture: amd64
|
||||
Maintainer: open-trade <info@rustdesk.com>
|
||||
Architecture: %s
|
||||
Maintainer: rustdesk <info@rustdesk.com>
|
||||
Homepage: https://rustdesk.com
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0
|
||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g
|
||||
Description: A remote control software.
|
||||
|
||||
""" % version
|
||||
""" % (version, get_arch())
|
||||
file = open(control_file_path, "w")
|
||||
file.write(content)
|
||||
file.close()
|
||||
@ -277,12 +308,54 @@ def build_flutter_deb(version, features):
|
||||
system2('flutter build linux --release')
|
||||
system2('mkdir -p tmpdeb/usr/bin/')
|
||||
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
|
||||
system2('mkdir -p tmpdeb/etc/rustdesk/')
|
||||
system2('mkdir -p tmpdeb/etc/pam.d/')
|
||||
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||
system2('mkdir -p tmpdeb/usr/share/applications/')
|
||||
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
|
||||
system2('rm tmpdeb/usr/bin/rustdesk || true')
|
||||
system2(
|
||||
'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/')
|
||||
f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/')
|
||||
system2(
|
||||
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||
system2(
|
||||
'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png')
|
||||
system2(
|
||||
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||
system2(
|
||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||
system2(
|
||||
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
|
||||
system2(
|
||||
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||
system2(
|
||||
'cp ../res/xorg.conf tmpdeb/etc/rustdesk/')
|
||||
system2(
|
||||
'cp ../res/pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
||||
system2(
|
||||
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
||||
|
||||
system2('mkdir -p tmpdeb/DEBIAN')
|
||||
generate_control_file(version)
|
||||
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
|
||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
||||
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
|
||||
|
||||
system2('/bin/rm -rf tmpdeb/')
|
||||
system2('/bin/rm -rf ../res/DEBIAN/control')
|
||||
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
|
||||
os.chdir("..")
|
||||
|
||||
def build_deb_from_folder(version, binary_folder):
|
||||
os.chdir('flutter')
|
||||
system2('mkdir -p tmpdeb/usr/bin/')
|
||||
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
|
||||
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||
system2('mkdir -p tmpdeb/usr/share/applications/')
|
||||
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
|
||||
system2('rm tmpdeb/usr/bin/rustdesk || true')
|
||||
system2(
|
||||
f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/')
|
||||
system2(
|
||||
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||
system2(
|
||||
@ -307,7 +380,6 @@ def build_flutter_deb(version, features):
|
||||
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
|
||||
os.chdir("..")
|
||||
|
||||
|
||||
def build_flutter_dmg(version, features):
|
||||
if not skip_cargo:
|
||||
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
|
||||
@ -329,7 +401,7 @@ def build_flutter_arch_manjaro(version, features):
|
||||
ffi_bindgen_function_refactor()
|
||||
os.chdir('flutter')
|
||||
system2('flutter build linux --release')
|
||||
system2('strip build/linux/x64/release/bundle/lib/librustdesk.so')
|
||||
system2(f'strip {flutter_build_dir}/lib/librustdesk.so')
|
||||
os.chdir('../res')
|
||||
system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
|
||||
|
||||
@ -344,11 +416,11 @@ def build_flutter_windows(version, features):
|
||||
system2('flutter build windows --release')
|
||||
os.chdir('..')
|
||||
shutil.copy2('target/release/deps/dylib_virtual_display.dll',
|
||||
flutter_win_target_dir)
|
||||
flutter_build_dir_2)
|
||||
os.chdir('libs/portable')
|
||||
system2('pip3 install -r requirements.txt')
|
||||
system2(
|
||||
f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe')
|
||||
f'python3 ./generate.py -f ../../{flutter_build_dir_2} -o . -e ../../{flutter_build_dir_2}/rustdesk.exe')
|
||||
os.chdir('../..')
|
||||
if os.path.exists('./rustdesk_portable.exe'):
|
||||
os.replace('./target/release/rustdesk-portable-packer.exe',
|
||||
@ -381,6 +453,12 @@ def main():
|
||||
if args.skip_cargo:
|
||||
skip_cargo = True
|
||||
portable = args.portable
|
||||
package = args.package
|
||||
if package:
|
||||
build_deb_from_folder(version, package)
|
||||
return
|
||||
res_dir = 'resources'
|
||||
external_resources(flutter, args, res_dir)
|
||||
if windows:
|
||||
# build virtual display dynamic library
|
||||
os.chdir('libs/virtual_display/dylib')
|
||||
@ -401,7 +479,12 @@ def main():
|
||||
else:
|
||||
print('Not signed')
|
||||
system2(
|
||||
f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe')
|
||||
f'cp -rf target/release/RustDesk.exe {res_dir}')
|
||||
os.chdir('libs/portable')
|
||||
system2('pip3 install -r requirements.txt')
|
||||
system2(
|
||||
f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe')
|
||||
system2('mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..')
|
||||
elif os.path.isfile('/usr/bin/pacman'):
|
||||
# pacman -S -needed base-devel
|
||||
system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
|
||||
@ -506,12 +589,21 @@ def main():
|
||||
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||
system2(
|
||||
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||
system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/')
|
||||
os.system('mkdir -p tmpdeb/etc/rustdesk/')
|
||||
os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||
os.system('mkdir -p tmpdeb/etc/X11/rustdesk/')
|
||||
os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/')
|
||||
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
|
||||
os.system('mkdir -p tmpdeb/etc/pam.d/')
|
||||
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
||||
system2('strip tmpdeb/usr/bin/rustdesk')
|
||||
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
|
||||
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
|
||||
system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
|
||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
||||
md5_file('etc/rustdesk/startwm.sh')
|
||||
md5_file('etc/X11/rustdesk/xorg.conf')
|
||||
md5_file('etc/pam.d/rustdesk')
|
||||
md5_file('usr/lib/rustdesk/libsciter-gtk.so')
|
||||
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
|
||||
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
|
||||
|
26
build.rs
26
build.rs
@ -41,20 +41,6 @@ fn build_manifest() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(windows, feature = "with_rc"))]
|
||||
fn build_rc_source() {
|
||||
use simple_rc::{generate_with_conf, Config, ConfigItem};
|
||||
generate_with_conf(&Config {
|
||||
outfile: "src/rc.rs".to_owned(),
|
||||
confs: vec![ConfigItem {
|
||||
inc: "resources".to_owned(),
|
||||
exc: vec![],
|
||||
suppressed_front: "resources".to_owned(),
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn install_oboe() {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
if target_os != "android" {
|
||||
@ -133,15 +119,15 @@ fn main() {
|
||||
gen_flutter_rust_bridge();
|
||||
// return;
|
||||
// }
|
||||
#[cfg(all(windows, feature = "with_rc"))]
|
||||
build_rc_source();
|
||||
#[cfg(all(windows, feature = "inline"))]
|
||||
build_manifest();
|
||||
#[cfg(windows)]
|
||||
build_windows();
|
||||
#[cfg(target_os = "macos")]
|
||||
build_mac();
|
||||
#[cfg(target_os = "macos")]
|
||||
println!("cargo:rustc-link-lib=framework=ApplicationServices");
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
if target_os == "macos" {
|
||||
#[cfg(target_os = "macos")]
|
||||
build_mac();
|
||||
println!("cargo:rustc-link-lib=framework=ApplicationServices");
|
||||
}
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
|
133
docs/CODE_OF_CONDUCT-PL.md
Normal file
133
docs/CODE_OF_CONDUCT-PL.md
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
# Kod postępowania Contributor Covenant Code of Conduct
|
||||
|
||||
## Nasza przysięga
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Nasze standardy
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[info@rustdesk.com](mailto:info@rustdesk.com).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||
at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
45
docs/CONTRIBUTING-PL.md
Normal file
45
docs/CONTRIBUTING-PL.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Współtworzenie RustDesk
|
||||
|
||||
RustDesk z zadowoleniem przyjmuje wkład od każdego. Oto wytyczne, jeśli chcesz nam pomóc:
|
||||
|
||||
## Współtwórcy
|
||||
|
||||
Contributions to RustDesk or its dependencies should be made in the form of GitHub
|
||||
pull requests. Each pull request will be reviewed by a core contributor
|
||||
(someone with permission to land patches) and either landed in the main tree or
|
||||
given feedback for changes that would be required. All contributions should
|
||||
follow this format, even those from core contributors.
|
||||
|
||||
Should you wish to work on an issue, please claim it first by commenting on
|
||||
the GitHub issue that you want to work on it. This is to prevent duplicated
|
||||
efforts from contributors on the same issue.
|
||||
|
||||
## Pull Request Checklist
|
||||
|
||||
- Branch from the master branch and, if needed, rebase to the current master
|
||||
branch before submitting your pull request. If it doesn't merge cleanly with
|
||||
master you may be asked to rebase your changes.
|
||||
|
||||
- Commits should be as small as possible, while ensuring that each commit is
|
||||
correct independently (i.e., each commit should compile and pass tests).
|
||||
|
||||
- Commits should be accompanied by a Developer Certificate of Origin
|
||||
(http://developercertificate.org) sign-off, which indicates that you (and
|
||||
your employer if applicable) agree to be bound by the terms of the
|
||||
[project license](../LICENCE). In git, this is the `-s` option to `git commit`
|
||||
|
||||
- If your patch is not getting reviewed or you need a specific person to review
|
||||
it, you can @-reply a reviewer asking for a review in the pull request or a
|
||||
comment, or you can ask for a review via [email](mailto:info@rustdesk.com).
|
||||
|
||||
- Add tests relevant to the fixed bug or new feature.
|
||||
|
||||
For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||
|
||||
## Kodeks postępowania
|
||||
|
||||
[Kodeks postępowania](CODE_OF_CONDUCT-PL.md)
|
||||
|
||||
## Komunikacja
|
||||
|
||||
RustDesk contributors frequent the [Discord](https://discord.gg/nDceKgxnkV).
|
14
docs/DEVCONTAINER-PL.md
Normal file
14
docs/DEVCONTAINER-PL.md
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
Po uruchomieniu devcontainer w kontenerze docker, tworzony jest plik binarny linux w trybue debugowania.
|
||||
|
||||
Obecnie devcontainer oferuje kompilowanie wersji dla linux i android w obu trybach - debugowania i wersji finalnej.
|
||||
|
||||
Poniżej tabela poleceń do uruchomienia z głównego folderu do tworzenia wybranych kompilacji.
|
||||
|
||||
Polecenie|Typ kompilacji|Tryb
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|debug
|
||||
|
@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Twój zdalny pulpit"><br>
|
||||
<a href="#darmowe-serwery-publiczne">Serwery</a> •
|
||||
<a href="#podstawowe-kroki-do-kompilacji">Kompilacja</a> •
|
||||
<a href="#jak-kompilować-za-pomocą-dockera">Docker</a> •
|
||||
@ -13,27 +13,45 @@ Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](http
|
||||
|
||||
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||
RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) pomoc w uruchomieniu programu.
|
||||
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
|
||||
|
||||
[**POBIERZ KOMPILACJE**](https://github.com/rustdesk/rustdesk/releases)
|
||||
RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](CONTRIBUTING-PL.md) pomoc w uruchomieniu programu.
|
||||
|
||||
[**PYTANIA I ODPOWIEDZI (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**POBIERANIE BINARIÓW**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**WERSJE TESTOWE (NIGHTLY)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
|
||||
## Darmowe Serwery Publiczne
|
||||
|
||||
Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska.
|
||||
| Lokalizacja | Dostawca | Specyfikacja |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Korea Płd. (Seul) | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||
| Niemcy | Hetzner | 2 vCPU / 4GB RAM |
|
||||
| Niemcy | Codext | 4 vCPU / 8GB RAM |
|
||||
| Finlandia (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
|
||||
| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
| Ukraina (Kijów) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
|
||||
|
||||
## Konterner Programisty (Dev Container)
|
||||
|
||||
[![Otwórz w Kontenerze programisty](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
|
||||
|
||||
Jeżeli masz zainstalowany VS Code i Docker, możesz kliknąć w powyższy link, aby rozpocząć. Kliknięcie spowoduje automatyczną instalację rozszrzenia Kontenera Programisty w VS Code (jeżeli wymagany), sklonuje kod źródłowy do kontenera, i przygotuje kontener do użycia.
|
||||
|
||||
Więcej informacji w pliku [DEVCONTAINER-PL.md](docs/DEVCONTAINER-PL.md) for more info.
|
||||
|
||||
## Zależności
|
||||
|
||||
Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać bibliotekę dynamiczną sciter samodzielnie.
|
||||
Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać samodzielnie bibliotekę sciter.
|
||||
|
||||
[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) |
|
||||
@ -43,7 +61,7 @@ Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobra
|
||||
|
||||
- Przygotuj środowisko programistyczne Rust i środowisko programowania C++
|
||||
|
||||
- Zainstaluj [vcpkg](https://github.com/microsoft/vcpkg), i ustaw `VCPKG_ROOT` env zmienną prawidłowo
|
||||
- Zainstaluj [vcpkg](https://github.com/microsoft/vcpkg), i ustaw prawidłowo zmienną `VCPKG_ROOT`
|
||||
|
||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
|
||||
- Linux/MacOS: vcpkg install libvpx libyuv opus
|
||||
@ -58,6 +76,12 @@ Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobra
|
||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
@ -82,7 +106,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus
|
||||
```
|
||||
|
||||
### Fix libvpx (For Fedora)
|
||||
### Popraw libvpx (Dla Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
@ -110,7 +134,31 @@ cargo run
|
||||
|
||||
### Zmień Wayland na X11 (Xorg)
|
||||
|
||||
RustDesk nie obsługuje Waylanda. Sprawdź [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) by skonfigurować Xorg jako domyślną sesję GNOME.
|
||||
RustDesk nie obsługuje Waylanda. Sprawdź [tutaj](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), jak skonfigurować Xorg jako domyślną sesję GNOME.
|
||||
|
||||
## Wspracie Wayland
|
||||
|
||||
Wygląda na to, że Wayland nie wspiera żadnego API do wysyłania naciśnięć klawiszy do innych okien. Dlatego rustdesk używa API z niższego poziomu, urządzenia o nazwie `/dev/uinput` (poziom jądra Linux).
|
||||
|
||||
Gdy po stronie kontrolowanej pracuje Wayland, musisz uruchomić program w następujący sposób:
|
||||
```bash
|
||||
# Start uinput service
|
||||
$ sudo rustdesk --service
|
||||
$ rustdesk
|
||||
```
|
||||
**Uwaga**: Nagrywanie ekranu Wayland wykorzystuje różne interfejsy. RustDesk obecnie obsługuje tylko org.freedesktop.portal.ScreenCast.
|
||||
```bash
|
||||
$ dbus-send --session --print-reply \
|
||||
--dest=org.freedesktop.portal.Desktop \
|
||||
/org/freedesktop/portal/desktop \
|
||||
org.freedesktop.DBus.Properties.Get \
|
||||
string:org.freedesktop.portal.ScreenCast string:version
|
||||
# Not support
|
||||
Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
|
||||
# Support
|
||||
method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
|
||||
variant uint32 4
|
||||
```
|
||||
|
||||
## Jak kompilować za pomocą Dockera
|
||||
|
||||
@ -134,7 +182,7 @@ Zauważ, że pierwsza kompilacja może potrwać dłużej zanim zależności zost
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Lub, jeśli uruchamiasz plik wykonywalny wersji:
|
||||
Lub jeśli uruchamiasz plik wykonywalny wersji:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
@ -144,16 +192,18 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
|
||||
|
||||
## Struktura plików
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: kodek wideo, config, wrapper tcp/udp, protobuf, funkcje fs do transferu plików i kilka innych funkcji użytkowych
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: kodek wideo, konfiguracja, obsługa tcp/udp, protobuf, funkcje systemu plików do transferu plików i kilka innych funkcji użytkowych
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: przechwytywanie ekranu
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: specyficzne dla danej platformy sterowanie klawiaturą/myszą
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/schowek/wejście(input)/wideo oraz połączenia sieciowe
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: uruchamia połączenie peer
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: specyficzny dla danej platformy kod
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: uruchamia połączenie bezpośrednie
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), czekanie na bezpośrednie (odpytywanie TCP) lub przekazywane połączenie
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kod specyficzny dla danej platformy
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: kod Flutter dla urządzeń mobilnych
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript dla Flutter - klient web
|
||||
|
||||
## Migawki(Snapshoty)
|
||||
## Zrzuty ekranu
|
||||
|
||||
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
|
||||
|
||||
|
9
docs/SECURITY-PL.md
Normal file
9
docs/SECURITY-PL.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Polityka bezpieczeństwa
|
||||
|
||||
## Zgłaszanie podatności
|
||||
|
||||
Bardzo cenimy sobie bezpieczeństwo projektu. Zachęcamy wszystkich użytkowników do zgłaszania nam wszelkich wykrytych luk.
|
||||
Jeżeli znajdziesz lukę w projekcie RustDesk, proszę zgłosić ją jak najszybciej wysyłając e-mail na adres info@rustdesk.com.
|
||||
|
||||
W tym momencie, nie mamy uruchomionego programu nagradzania za wykryte błędy. Jesteśmy małym zespołem próbującym rozwiązywać duże problemy.
|
||||
Prosimy o odpowidzialne zgłaszanie wszelkich podatności w zabezpieczeniach, abyśmy mogli kontynuować tworzenie bezpiecznej aplikacji dla całej społeczności.
|
28
examples/custom_plugin/Cargo.toml
Normal file
28
examples/custom_plugin/Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "custom_plugin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "custom_plugin"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["flutter"]
|
||||
flutter = []
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
rustdesk = { path = "../../", version = "1.2.0", features = ["flutter"]}
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
#opt-level = 'z' # only have smaller size after strip
|
||||
rpath = true
|
30
examples/custom_plugin/src/lib.rs
Normal file
30
examples/custom_plugin/src/lib.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use librustdesk::api::RustDeskApiTable;
|
||||
/// This file demonstrates how to write a custom plugin for RustDesk.
|
||||
use std::ffi::{c_char, c_int, CString};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref PLUGIN_NAME: CString = CString::new("A Template Rust Plugin").unwrap();
|
||||
pub static ref PLUGIN_ID: CString = CString::new("TemplatePlugin").unwrap();
|
||||
// Do your own logic based on the API provided by RustDesk.
|
||||
pub static ref API: RustDeskApiTable = RustDeskApiTable::default();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn plugin_name() -> *const c_char {
|
||||
return PLUGIN_NAME.as_ptr();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn plugin_id() -> *const c_char {
|
||||
return PLUGIN_ID.as_ptr();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn plugin_init() -> c_int {
|
||||
return 0 as _;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn plugin_dispose() -> c_int {
|
||||
return 0 as _;
|
||||
}
|
@ -58,13 +58,12 @@ function build {
|
||||
fi
|
||||
make clean
|
||||
./configure --target=$LIBVPX_TARGET \
|
||||
--enable-pic --disable-vp8 \
|
||||
--enable-pic
|
||||
--disable-webm-io \
|
||||
--disable-unit-tests \
|
||||
--disable-examples \
|
||||
--disable-libyuv \
|
||||
--disable-postproc \
|
||||
--disable-vp8 \
|
||||
--disable-tools \
|
||||
--disable-docs \
|
||||
--prefix=$PREFIX
|
||||
@ -122,4 +121,4 @@ build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc
|
||||
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
|
||||
|
||||
# rm -rf build/libvpx
|
||||
# rm -rf build/oboe
|
||||
# rm -rf build/oboe
|
||||
|
@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>9.0</string>
|
||||
<string>11.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '9.0'
|
||||
# platform :ios, '11.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
@ -1,194 +1,146 @@
|
||||
PODS:
|
||||
- device_info (0.0.1):
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (8.15.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (8.15.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 8.15.0)
|
||||
- Firebase/CoreOnly (8.15.0):
|
||||
- FirebaseCore (= 8.15.0)
|
||||
- firebase_analytics (9.1.8):
|
||||
- Firebase/Analytics (= 8.15.0)
|
||||
- firebase_core
|
||||
- DKImagePickerController/Core (4.3.4):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.4)
|
||||
- DKImagePickerController/PhotoGallery (4.3.4):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.4)
|
||||
- DKPhotoGallery (0.0.17):
|
||||
- DKPhotoGallery/Core (= 0.0.17)
|
||||
- DKPhotoGallery/Model (= 0.0.17)
|
||||
- DKPhotoGallery/Preview (= 0.0.17)
|
||||
- DKPhotoGallery/Resource (= 0.0.17)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- firebase_core (1.17.0):
|
||||
- Firebase/CoreOnly (= 8.15.0)
|
||||
- Flutter
|
||||
- FirebaseAnalytics (8.15.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 8.15.0)
|
||||
- FirebaseCore (~> 8.0)
|
||||
- FirebaseInstallations (~> 8.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||
- GoogleUtilities/Network (~> 7.7)
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||
- nanopb (~> 2.30908.0)
|
||||
- FirebaseAnalytics/AdIdSupport (8.15.0):
|
||||
- FirebaseCore (~> 8.0)
|
||||
- FirebaseInstallations (~> 8.0)
|
||||
- GoogleAppMeasurement (= 8.15.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||
- GoogleUtilities/Network (~> 7.7)
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||
- nanopb (~> 2.30908.0)
|
||||
- FirebaseCore (8.15.0):
|
||||
- FirebaseCoreDiagnostics (~> 8.0)
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- GoogleUtilities/Logger (~> 7.7)
|
||||
- FirebaseCoreDiagnostics (8.15.0):
|
||||
- GoogleDataTransport (~> 9.1)
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- GoogleUtilities/Logger (~> 7.7)
|
||||
- nanopb (~> 2.30908.0)
|
||||
- FirebaseInstallations (8.15.0):
|
||||
- FirebaseCore (~> 8.0)
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- GoogleUtilities/UserDefaults (~> 7.7)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- Flutter (1.0.0)
|
||||
- GoogleAppMeasurement (8.15.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 8.15.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||
- GoogleUtilities/Network (~> 7.7)
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||
- nanopb (~> 2.30908.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (8.15.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 8.15.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||
- GoogleUtilities/Network (~> 7.7)
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||
- nanopb (~> 2.30908.0)
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (8.15.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||
- GoogleUtilities/Network (~> 7.7)
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||
- nanopb (~> 2.30908.0)
|
||||
- GoogleDataTransport (9.1.4):
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleUtilities/AppDelegateSwizzler (7.7.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network
|
||||
- GoogleUtilities/Environment (7.7.0):
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleUtilities/Logger (7.7.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/MethodSwizzler (7.7.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network (7.7.0):
|
||||
- GoogleUtilities/Logger
|
||||
- "GoogleUtilities/NSData+zlib"
|
||||
- GoogleUtilities/Reachability
|
||||
- "GoogleUtilities/NSData+zlib (7.7.0)"
|
||||
- GoogleUtilities/Reachability (7.7.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/UserDefaults (7.7.0):
|
||||
- GoogleUtilities/Logger
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner (5.0.11)
|
||||
- nanopb (2.30908.0):
|
||||
- nanopb/decode (= 2.30908.0)
|
||||
- nanopb/encode (= 2.30908.0)
|
||||
- nanopb/decode (2.30908.0)
|
||||
- nanopb/encode (2.30908.0)
|
||||
- package_info (0.0.1):
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_ios (0.0.1):
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- PromisesObjC (2.1.0)
|
||||
- FlutterMacOS
|
||||
- qr_code_scanner (0.2.0):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- SDWebImage (5.15.5):
|
||||
- SDWebImage/Core (= 5.15.5)
|
||||
- SDWebImage/Core (5.15.5)
|
||||
- sqflite (0.0.2):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- SwiftyGif (5.4.4)
|
||||
- uni_links (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- wakelock (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- device_info (from `.symlinks/plugins/device_info/ios`)
|
||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- package_info (from `.symlinks/plugins/package_info/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
|
||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- uni_links (from `.symlinks/plugins/uni_links/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Firebase
|
||||
- FirebaseAnalytics
|
||||
- FirebaseCore
|
||||
- FirebaseCoreDiagnostics
|
||||
- FirebaseInstallations
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- FMDB
|
||||
- MTBBarcodeScanner
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
device_info:
|
||||
:path: ".symlinks/plugins/device_info/ios"
|
||||
firebase_analytics:
|
||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||
firebase_core:
|
||||
:path: ".symlinks/plugins/firebase_core/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_keyboard_visibility:
|
||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
package_info:
|
||||
:path: ".symlinks/plugins/package_info/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
shared_preferences_ios:
|
||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
uni_links:
|
||||
:path: ".symlinks/plugins/uni_links/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||
wakelock:
|
||||
:path: ".symlinks/plugins/wakelock/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info: d7d233b645a32c40dfdc212de5cf646ca482f175
|
||||
Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d
|
||||
firebase_analytics: 92d27947c7516981cabdc0acbb33cd0687bcda44
|
||||
firebase_core: aa1b92020533f5c23955e388c347c58fd64f8627
|
||||
FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa
|
||||
FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1
|
||||
FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb
|
||||
FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
|
||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e
|
||||
GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b
|
||||
GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
|
||||
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
|
||||
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
|
||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
|
||||
PODFILE CHECKSUM: a00077baecbb97321490c14848fceed3893ca92a
|
||||
PODFILE CHECKSUM: c649b4e69a3086d323110011d04604e416ad0dcd
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.12.0
|
||||
|
@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@ -228,6 +228,7 @@
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
@ -264,6 +265,7 @@
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
@ -338,6 +340,7 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
@ -351,7 +354,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@ -414,6 +417,7 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
@ -433,7 +437,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@ -469,6 +473,7 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
@ -482,7 +487,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
@ -13,9 +13,9 @@ import Flutter
|
||||
}
|
||||
|
||||
public func dummyMethodToEnforceBundling() {
|
||||
get_rgba();
|
||||
free_rgba(nil);
|
||||
get_by_name("", "");
|
||||
set_by_name("", "");
|
||||
// get_rgba();
|
||||
// free_rgba(nil);
|
||||
// get_by_name("", "");
|
||||
// set_by_name("", "");
|
||||
}
|
||||
}
|
||||
|
@ -49,5 +49,9 @@
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs photo library access to get QR codes from image</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -1,4 +1,4 @@
|
||||
void* get_rgba();
|
||||
void free_rgba(void*);
|
||||
void set_by_name(const char*, const char*);
|
||||
const char* get_by_name(const char*, const char*);
|
||||
//void* get_rgba();
|
||||
//void free_rgba(void*);
|
||||
//void set_by_name(const char*, const char*);
|
||||
//const char* get_by_name(const char*, const char*);
|
||||
|
@ -43,6 +43,7 @@ final isIOS = Platform.isIOS;
|
||||
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
|
||||
var isWeb = false;
|
||||
var isWebDesktop = false;
|
||||
var isMobile = isAndroid || isIOS;
|
||||
var version = "";
|
||||
int androidVersion = 0;
|
||||
|
||||
@ -186,6 +187,71 @@ class MyTheme {
|
||||
static const Color button = Color(0xFF2C8CFF);
|
||||
static const Color hoverBorder = Color(0xFF999999);
|
||||
|
||||
// ListTile
|
||||
static const ListTileThemeData listTileTheme = ListTileThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Checkbox
|
||||
static const CheckboxThemeData checkboxTheme = CheckboxThemeData(
|
||||
splashRadius: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// TextButton
|
||||
// Value is used to calculate "dialog.actionsPadding"
|
||||
static const double mobileTextButtonPaddingLR = 20;
|
||||
|
||||
// TextButton on mobile needs a fixed padding, otherwise small buttons
|
||||
// like "OK" has a larger left/right padding.
|
||||
static TextButtonThemeData mobileTextButtonTheme = TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(horizontal: mobileTextButtonPaddingLR),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Dialogs
|
||||
static const double dialogPadding = 24;
|
||||
|
||||
// padding bottom depends on content (some dialogs has no content)
|
||||
static EdgeInsets dialogTitlePadding({bool content = true}) {
|
||||
final double p = dialogPadding;
|
||||
|
||||
return EdgeInsets.fromLTRB(p, p, p, content ? 0 : p);
|
||||
}
|
||||
|
||||
// padding bottom depends on actions (mobile has dialogs without actions)
|
||||
static EdgeInsets dialogContentPadding({bool actions = true}) {
|
||||
final double p = dialogPadding;
|
||||
|
||||
return isDesktop
|
||||
? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p)
|
||||
: EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p);
|
||||
}
|
||||
|
||||
static EdgeInsets dialogActionsPadding() {
|
||||
final double p = dialogPadding;
|
||||
|
||||
return isDesktop
|
||||
? EdgeInsets.fromLTRB(p, 0, p, (p - 4))
|
||||
: EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2));
|
||||
}
|
||||
|
||||
static EdgeInsets dialogButtonPadding = isDesktop
|
||||
? EdgeInsets.only(left: dialogPadding)
|
||||
: EdgeInsets.only(left: dialogPadding / 3);
|
||||
|
||||
static ThemeData lightTheme = ThemeData(
|
||||
brightness: Brightness.light,
|
||||
hoverColor: Color.fromARGB(255, 224, 224, 224),
|
||||
@ -236,7 +302,7 @@ class MyTheme {
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
: mobileTextButtonTheme,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: MyTheme.accent,
|
||||
@ -254,21 +320,8 @@ class MyTheme {
|
||||
),
|
||||
),
|
||||
),
|
||||
checkboxTheme: const CheckboxThemeData(
|
||||
splashRadius: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkboxTheme: checkboxTheme,
|
||||
listTileTheme: listTileTheme,
|
||||
menuBarTheme: MenuBarThemeData(
|
||||
style:
|
||||
MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))),
|
||||
@ -334,7 +387,7 @@ class MyTheme {
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
: mobileTextButtonTheme,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: MyTheme.accent,
|
||||
@ -357,21 +410,8 @@ class MyTheme {
|
||||
),
|
||||
),
|
||||
),
|
||||
checkboxTheme: const CheckboxThemeData(
|
||||
splashRadius: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkboxTheme: checkboxTheme,
|
||||
listTileTheme: listTileTheme,
|
||||
menuBarTheme: MenuBarThemeData(
|
||||
style: MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Color(0xFF121212)))),
|
||||
@ -757,6 +797,7 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
fontWeight: FontWeight.w300,
|
||||
@ -771,6 +812,10 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
// - Remove argument "contentPadding", no need for it, all should look the same.
|
||||
// - Remove "required" for argument "content". See simple confirm dialog "delete peer", only title and actions are used. No need to "content: SizedBox.shrink()".
|
||||
// - Make dead code alive, transform arguments "onSubmit" and "onCancel" into correspondenting buttons "ConfirmOkButton", "CancelButton".
|
||||
class CustomAlertDialog extends StatelessWidget {
|
||||
const CustomAlertDialog(
|
||||
{Key? key,
|
||||
@ -798,8 +843,8 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!scopeNode.hasFocus) scopeNode.requestFocus();
|
||||
});
|
||||
const double padding = 30;
|
||||
bool tabTapped = false;
|
||||
|
||||
return FocusScope(
|
||||
node: scopeNode,
|
||||
autofocus: true,
|
||||
@ -824,22 +869,18 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: AlertDialog(
|
||||
scrollable: true,
|
||||
title: title,
|
||||
titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0),
|
||||
contentPadding: EdgeInsets.fromLTRB(
|
||||
contentPadding ?? padding,
|
||||
25,
|
||||
contentPadding ?? padding,
|
||||
actions is List ? 10 : padding,
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: contentBoxConstraints,
|
||||
child: content,
|
||||
),
|
||||
actions: actions,
|
||||
actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding),
|
||||
),
|
||||
scrollable: true,
|
||||
title: title,
|
||||
content: ConstrainedBox(
|
||||
constraints: contentBoxConstraints,
|
||||
child: content,
|
||||
),
|
||||
actions: actions,
|
||||
titlePadding: MyTheme.dialogTitlePadding(content: content != null),
|
||||
contentPadding:
|
||||
MyTheme.dialogContentPadding(actions: actions is List),
|
||||
actionsPadding: MyTheme.dialogActionsPadding(),
|
||||
buttonPadding: MyTheme.dialogButtonPadding),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1115,38 +1156,23 @@ class AndroidPermissionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move this to mobile/widgets.
|
||||
// Used only for mobile, pages remote, settings, dialog
|
||||
// TODO remove argument contentPadding, it’s not used, getToggle() has not
|
||||
RadioListTile<T> getRadio<T>(
|
||||
String name, T toValue, T curValue, void Function(T?) onChange,
|
||||
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
|
||||
{EdgeInsetsGeometry? contentPadding}) {
|
||||
return RadioListTile<T>(
|
||||
contentPadding: contentPadding,
|
||||
contentPadding: contentPadding ?? EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
title: Text(translate(name)),
|
||||
title: title,
|
||||
value: toValue,
|
||||
groupValue: curValue,
|
||||
onChanged: onChange,
|
||||
dense: true,
|
||||
);
|
||||
}
|
||||
|
||||
CheckboxListTile getToggle(
|
||||
String id, void Function(void Function()) setState, option, name,
|
||||
{FFI? ffi}) {
|
||||
final opt = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||
return CheckboxListTile(
|
||||
value: opt,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
bind.sessionToggleOption(id: id, value: option);
|
||||
});
|
||||
if (option == "show-quality-monitor") {
|
||||
(ffi ?? gFFI).qualityMonitorModel.checkShowQualityMonitor(id);
|
||||
}
|
||||
},
|
||||
dense: true,
|
||||
title: Text(translate(name)));
|
||||
}
|
||||
|
||||
/// find ffi, tag is Remote ID
|
||||
/// for session specific usage
|
||||
FFI ffi(String? tag) {
|
||||
@ -1522,14 +1548,14 @@ bool checkArguments() {
|
||||
return false;
|
||||
}
|
||||
String? id =
|
||||
kBootArgs.length < connectIndex + 1 ? null : kBootArgs[connectIndex + 1];
|
||||
kBootArgs.length <= connectIndex + 1 ? null : kBootArgs[connectIndex + 1];
|
||||
String? password =
|
||||
kBootArgs.length < connectIndex + 2 ? null : kBootArgs[connectIndex + 2];
|
||||
kBootArgs.length <= connectIndex + 2 ? null : kBootArgs[connectIndex + 2];
|
||||
if (password != null && password.startsWith("--")) {
|
||||
password = null;
|
||||
}
|
||||
final switchUuidIndex = kBootArgs.indexOf("--switch_uuid");
|
||||
String? switchUuid = kBootArgs.length < switchUuidIndex + 1
|
||||
String? switchUuid = kBootArgs.length <= switchUuidIndex + 1
|
||||
? null
|
||||
: kBootArgs[switchUuidIndex + 1];
|
||||
if (id != null) {
|
||||
@ -1574,8 +1600,10 @@ bool callUniLinksUriHandler(Uri uri) {
|
||||
final peerId = uri.path.substring("/new/".length);
|
||||
var param = uri.queryParameters;
|
||||
String? switch_uuid = param["switch_uuid"];
|
||||
String? password = param["password"];
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid);
|
||||
rustDeskWinManager.newRemoteDesktop(peerId,
|
||||
password: password, switch_uuid: switch_uuid);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@ -2007,3 +2035,12 @@ Widget futureBuilder(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onCopyFingerprint(String value) {
|
||||
if (value.isNotEmpty) {
|
||||
Clipboard.setData(ClipboardData(text: value));
|
||||
showToast('$value\n${translate("Copied")}');
|
||||
} else {
|
||||
showToast(translate("no fingerprints"));
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,29 @@ class ConnectionTypeState {
|
||||
Get.find<ConnectionType>(tag: tag(id));
|
||||
}
|
||||
|
||||
class FingerprintState {
|
||||
static String tag(String id) => 'fingerprint_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxString state = ''.obs;
|
||||
Get.put(state, tag: key);
|
||||
} else {
|
||||
Get.find<RxString>(tag: key).value = '';
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static RxString find(String id) => Get.find<RxString>(tag: tag(id));
|
||||
}
|
||||
|
||||
class ShowRemoteCursorState {
|
||||
static String tag(String id) => 'show_remote_cursor_$id';
|
||||
|
||||
@ -261,3 +284,25 @@ class PeerStringOption {
|
||||
static RxString find(String id, String opt) =>
|
||||
Get.find<RxString>(tag: tag(id, opt));
|
||||
}
|
||||
|
||||
initSharedStates(String id) {
|
||||
PrivacyModeState.init(id);
|
||||
BlockInputState.init(id);
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
RemoteCursorMovedState.init(id);
|
||||
FingerprintState.init(id);
|
||||
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
||||
}
|
||||
|
||||
removeSharedStates(String id) {
|
||||
PrivacyModeState.delete(id);
|
||||
BlockInputState.delete(id);
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
RemoteCursorMovedState.delete(id);
|
||||
FingerprintState.delete(id);
|
||||
PeerBoolOption.delete(id, 'zoom-cursor');
|
||||
}
|
||||
|
@ -1,10 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
|
||||
void clientClose(String id, OverlayDialogManager dialogManager) {
|
||||
msgBox(id, 'info', 'Close', 'Are you sure to close the connection?', '',
|
||||
dialogManager);
|
||||
}
|
||||
|
||||
abstract class ValidationRule {
|
||||
String get name;
|
||||
bool validate(String value);
|
||||
@ -290,3 +300,972 @@ Future<String> changeDirectAccessPort(
|
||||
});
|
||||
return controller.text;
|
||||
}
|
||||
|
||||
class DialogTextField extends StatelessWidget {
|
||||
final String title;
|
||||
final String? hintText;
|
||||
final bool obscureText;
|
||||
final String? errorText;
|
||||
final String? helperText;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final TextEditingController controller;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
static const kUsernameTitle = 'Username';
|
||||
static const kUsernameIcon = Icon(Icons.account_circle_outlined);
|
||||
static const kPasswordTitle = 'Password';
|
||||
static const kPasswordIcon = Icon(Icons.lock_outline);
|
||||
|
||||
DialogTextField(
|
||||
{Key? key,
|
||||
this.focusNode,
|
||||
this.obscureText = false,
|
||||
this.errorText,
|
||||
this.helperText,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.hintText,
|
||||
required this.title,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: title,
|
||||
hintText: hintText,
|
||||
prefixIcon: prefixIcon,
|
||||
suffixIcon: suffixIcon,
|
||||
helperText: helperText,
|
||||
helperMaxLines: 8,
|
||||
errorText: errorText,
|
||||
),
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
obscureText: obscureText,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(vertical: 4.0);
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordWidget extends StatefulWidget {
|
||||
PasswordWidget({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
this.autoFocus = true,
|
||||
this.hintText,
|
||||
this.errorText,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
final bool autoFocus;
|
||||
final String? hintText;
|
||||
final String? errorText;
|
||||
|
||||
@override
|
||||
State<PasswordWidget> createState() => _PasswordWidgetState();
|
||||
}
|
||||
|
||||
class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
bool _passwordVisible = false;
|
||||
final _focusNode = FocusNode();
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.autoFocus) {
|
||||
_timer =
|
||||
Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_focusNode.unfocus();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DialogTextField(
|
||||
title: translate(DialogTextField.kPasswordTitle),
|
||||
hintText: translate(widget.hintText ?? 'Enter your password'),
|
||||
controller: widget.controller,
|
||||
prefixIcon: DialogTextField.kPasswordIcon,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
// Based on passwordVisible state choose the icon
|
||||
_passwordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
// Update the state i.e. toggle the state of passwordVisible variable
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
obscureText: !_passwordVisible,
|
||||
errorText: widget.errorText,
|
||||
focusNode: _focusNode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void wrongPasswordDialog(
|
||||
String id, OverlayDialogManager dialogManager, type, title, text) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
cancel() {
|
||||
close();
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
submit() {
|
||||
enterPasswordDialog(id, dialogManager);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
onPressed: cancel,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'Retry',
|
||||
onPressed: submit,
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async {
|
||||
await _connectDialog(
|
||||
id,
|
||||
dialogManager,
|
||||
passwordController: TextEditingController(),
|
||||
);
|
||||
}
|
||||
|
||||
void enterUserLoginDialog(String id, OverlayDialogManager dialogManager) async {
|
||||
await _connectDialog(
|
||||
id,
|
||||
dialogManager,
|
||||
osUsernameController: TextEditingController(),
|
||||
osPasswordController: TextEditingController(),
|
||||
);
|
||||
}
|
||||
|
||||
void enterUserLoginAndPasswordDialog(
|
||||
String id, OverlayDialogManager dialogManager) async {
|
||||
await _connectDialog(
|
||||
id,
|
||||
dialogManager,
|
||||
osUsernameController: TextEditingController(),
|
||||
osPasswordController: TextEditingController(),
|
||||
passwordController: TextEditingController(),
|
||||
);
|
||||
}
|
||||
|
||||
_connectDialog(
|
||||
String id,
|
||||
OverlayDialogManager dialogManager, {
|
||||
TextEditingController? osUsernameController,
|
||||
TextEditingController? osPasswordController,
|
||||
TextEditingController? passwordController,
|
||||
}) async {
|
||||
var rememberPassword = false;
|
||||
if (passwordController != null) {
|
||||
rememberPassword = await bind.sessionGetRemember(id: id) ?? false;
|
||||
}
|
||||
var rememberAccount = false;
|
||||
if (osUsernameController != null) {
|
||||
rememberAccount = await bind.sessionGetRemember(id: id) ?? false;
|
||||
}
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
cancel() {
|
||||
close();
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
submit() {
|
||||
final osUsername = osUsernameController?.text.trim() ?? '';
|
||||
final osPassword = osPasswordController?.text.trim() ?? '';
|
||||
final password = passwordController?.text.trim() ?? '';
|
||||
if (passwordController != null && password.isEmpty) return;
|
||||
if (rememberAccount) {
|
||||
bind.sessionPeerOption(id: id, name: 'os-username', value: osUsername);
|
||||
bind.sessionPeerOption(id: id, name: 'os-password', value: osPassword);
|
||||
}
|
||||
gFFI.login(
|
||||
osUsername,
|
||||
osPassword,
|
||||
id,
|
||||
password,
|
||||
rememberPassword,
|
||||
);
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
}
|
||||
|
||||
descWidget(String text) {
|
||||
return Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 3,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
rememberWidget(
|
||||
String desc,
|
||||
bool remember,
|
||||
ValueChanged<bool?>? onChanged,
|
||||
) {
|
||||
return CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(desc),
|
||||
value: remember,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
osAccountWidget() {
|
||||
if (osUsernameController == null || osPasswordController == null) {
|
||||
return Offstage();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
descWidget(translate('login_linux_tip')),
|
||||
DialogTextField(
|
||||
title: translate(DialogTextField.kUsernameTitle),
|
||||
controller: osUsernameController,
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: null,
|
||||
),
|
||||
PasswordWidget(
|
||||
controller: osPasswordController,
|
||||
autoFocus: false,
|
||||
),
|
||||
rememberWidget(
|
||||
translate('remember_account_tip'),
|
||||
rememberAccount,
|
||||
(v) {
|
||||
if (v != null) {
|
||||
setState(() => rememberAccount = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
passwdWidget() {
|
||||
if (passwordController == null) {
|
||||
return Offstage();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
descWidget(translate('verify_rustdesk_password_tip')),
|
||||
PasswordWidget(
|
||||
controller: passwordController,
|
||||
autoFocus: osUsernameController == null,
|
||||
),
|
||||
rememberWidget(
|
||||
translate('Remember password'),
|
||||
rememberPassword,
|
||||
(v) {
|
||||
if (v != null) {
|
||||
setState(() => rememberPassword = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('Password Required')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
osAccountWidget(),
|
||||
osUsernameController == null || passwordController == null
|
||||
? Offstage()
|
||||
: Container(height: 12),
|
||||
passwdWidget(),
|
||||
]),
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: cancel,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'OK',
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showWaitUacDialog(
|
||||
String id, OverlayDialogManager dialogManager, String type) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show(
|
||||
tag: '$id-wait-uac',
|
||||
(setState, close) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
|
||||
));
|
||||
}
|
||||
|
||||
// Another username && password dialog?
|
||||
void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) {
|
||||
RxString groupValue = ''.obs;
|
||||
RxString errUser = ''.obs;
|
||||
RxString errPwd = ''.obs;
|
||||
TextEditingController userController = TextEditingController();
|
||||
TextEditingController pwdController = TextEditingController();
|
||||
|
||||
void onRadioChanged(String? value) {
|
||||
if (value != null) {
|
||||
groupValue.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
const minTextStyle = TextStyle(fontSize: 14);
|
||||
|
||||
var content = Obx(() => Column(children: [
|
||||
Row(
|
||||
children: [
|
||||
Radio(
|
||||
value: '',
|
||||
groupValue: groupValue.value,
|
||||
onChanged: onRadioChanged),
|
||||
Expanded(
|
||||
child:
|
||||
Text(translate('Ask the remote user for authentication'))),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
translate(
|
||||
'Choose this if the remote account is administrator'),
|
||||
style: TextStyle(fontSize: 13))
|
||||
.marginOnly(left: 40),
|
||||
).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Radio(
|
||||
value: 'logon',
|
||||
groupValue: groupValue.value,
|
||||
onChanged: onRadioChanged),
|
||||
Expanded(
|
||||
child: Text(translate(
|
||||
'Transmit the username and password of administrator')),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${translate('Username')}:',
|
||||
style: minTextStyle,
|
||||
).marginOnly(right: 10)),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextField(
|
||||
controller: userController,
|
||||
style: minTextStyle,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 15),
|
||||
hintText: translate('eg: admin'),
|
||||
errorText: errUser.isEmpty ? null : errUser.value),
|
||||
onChanged: (s) {
|
||||
if (s.isNotEmpty) {
|
||||
errUser.value = '';
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
).marginOnly(left: 40),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${translate('Password')}:',
|
||||
style: minTextStyle,
|
||||
).marginOnly(right: 10)),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextField(
|
||||
controller: pwdController,
|
||||
obscureText: true,
|
||||
style: minTextStyle,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 15),
|
||||
errorText: errPwd.isEmpty ? null : errPwd.value),
|
||||
onChanged: (s) {
|
||||
if (s.isNotEmpty) {
|
||||
errPwd.value = '';
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(left: 40),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(translate('still_click_uac_tip'),
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold))
|
||||
.marginOnly(top: 20)),
|
||||
]));
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show(tag: '$id-request-elevation', (setState, close) {
|
||||
void submit() {
|
||||
if (groupValue.value == 'logon') {
|
||||
if (userController.text.isEmpty) {
|
||||
errUser.value = translate('Empty Username');
|
||||
return;
|
||||
}
|
||||
if (pwdController.text.isEmpty) {
|
||||
errPwd.value = translate('Empty Password');
|
||||
return;
|
||||
}
|
||||
bind.sessionElevateWithLogon(
|
||||
id: id,
|
||||
username: userController.text,
|
||||
password: pwdController.text);
|
||||
} else {
|
||||
bind.sessionElevateDirect(id: id);
|
||||
}
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Request Elevation')),
|
||||
content: content,
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showOnBlockDialog(
|
||||
String id,
|
||||
String type,
|
||||
String title,
|
||||
String text,
|
||||
OverlayDialogManager dialogManager,
|
||||
) {
|
||||
if (dialogManager.existing('$id-wait-uac') ||
|
||||
dialogManager.existing('$id-request-elevation')) {
|
||||
return;
|
||||
}
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
void submit() {
|
||||
close();
|
||||
showRequestElevationDialog(id, dialogManager);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title,
|
||||
"${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"),
|
||||
actions: [
|
||||
dialogButton('Wait', onPressed: close, isOutline: true),
|
||||
dialogButton('Request Elevation', onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showElevationError(String id, String type, String title, String text,
|
||||
OverlayDialogManager dialogManager) {
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
void submit() {
|
||||
close();
|
||||
showRequestElevationDialog(id, dialogManager);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: () {
|
||||
close();
|
||||
}, isOutline: true),
|
||||
dialogButton('Retry', onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showWaitAcceptDialog(String id, String type, String title, String text,
|
||||
OverlayDialogManager dialogManager) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
onCancel() {
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: onCancel, isOutline: true),
|
||||
],
|
||||
onCancel: onCancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showRestartRemoteDevice(
|
||||
PeerInfo pi, String id, OverlayDialogManager dialogManager) async {
|
||||
final res =
|
||||
await dialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
|
||||
Flexible(
|
||||
child: Text(translate("Restart Remote Device"))
|
||||
.paddingOnly(left: 10)),
|
||||
]),
|
||||
content: Text(
|
||||
"${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: () => close(true),
|
||||
),
|
||||
],
|
||||
onCancel: close,
|
||||
onSubmit: () => close(true),
|
||||
));
|
||||
if (res == true) bind.sessionRestartRemoteDevice(id: id);
|
||||
}
|
||||
|
||||
showSetOSPassword(
|
||||
String id,
|
||||
bool login,
|
||||
OverlayDialogManager dialogManager,
|
||||
) async {
|
||||
final controller = TextEditingController();
|
||||
var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != '';
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: 'os-password', value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: 'auto-login', value: autoLogin ? 'Y' : '');
|
||||
if (text != '' && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('OS Password')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
showSetOSAccount(
|
||||
String id,
|
||||
OverlayDialogManager dialogManager,
|
||||
) async {
|
||||
final usernameController = TextEditingController();
|
||||
final passwdController = TextEditingController();
|
||||
var username = await bind.sessionGetOption(id: id, arg: 'os-username') ?? '';
|
||||
var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
|
||||
usernameController.text = username;
|
||||
passwdController.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
final username = usernameController.text.trim();
|
||||
final password = usernameController.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: 'os-username', value: username);
|
||||
bind.sessionPeerOption(id: id, name: 'os-password', value: password);
|
||||
close();
|
||||
}
|
||||
|
||||
descWidget(String text) {
|
||||
return Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 3,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('OS Account')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
descWidget(translate("os_account_desk_tip")),
|
||||
DialogTextField(
|
||||
title: translate(DialogTextField.kUsernameTitle),
|
||||
controller: usernameController,
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: null,
|
||||
),
|
||||
PasswordWidget(controller: passwdController),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
showAuditDialog(String id, dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
var text = controller.text.trim();
|
||||
if (text != '') {
|
||||
bind.sessionSendNote(id: id, note: text);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
late final focusNode = FocusNode(
|
||||
onKey: (FocusNode node, RawKeyEvent evt) {
|
||||
if (evt.logicalKey.keyLabel == 'Enter') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
int pos = controller.selection.base.offset;
|
||||
controller.text =
|
||||
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
|
||||
controller.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: pos + 1));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (evt.logicalKey.keyLabel == 'Esc') {
|
||||
if (evt is RawKeyDownEvent) {
|
||||
close();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Note')),
|
||||
content: SizedBox(
|
||||
width: 250,
|
||||
height: 120,
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: const InputDecoration.collapsed(
|
||||
hintText: 'input note here',
|
||||
),
|
||||
maxLines: null,
|
||||
maxLength: 256,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
)),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit)
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showConfirmSwitchSidesDialog(
|
||||
String id, OverlayDialogManager dialogManager) async {
|
||||
dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
await bind.sessionSwitchSides(id: id);
|
||||
closeConnection(id: id);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: msgboxContent('info', 'Switch Sides',
|
||||
'Please confirm if you want to share your desktop?'),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
customImageQualityDialog(String id, FFI ffi) async {
|
||||
double qualityInitValue = 50;
|
||||
double fpsInitValue = 30;
|
||||
bool qualitySet = false;
|
||||
bool fpsSet = false;
|
||||
setCustomValues({double? quality, double? fps}) async {
|
||||
if (quality != null) {
|
||||
qualitySet = true;
|
||||
await bind.sessionSetCustomImageQuality(id: id, value: quality.toInt());
|
||||
}
|
||||
if (fps != null) {
|
||||
fpsSet = true;
|
||||
await bind.sessionSetCustomFps(id: id, fps: fps.toInt());
|
||||
}
|
||||
if (!qualitySet) {
|
||||
qualitySet = true;
|
||||
await bind.sessionSetCustomImageQuality(
|
||||
id: id, value: qualityInitValue.toInt());
|
||||
}
|
||||
if (!fpsSet) {
|
||||
fpsSet = true;
|
||||
await bind.sessionSetCustomFps(id: id, fps: fpsInitValue.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
final btnClose = dialogButton('Close', onPressed: () async {
|
||||
await setCustomValues();
|
||||
ffi.dialogManager.dismissAll();
|
||||
});
|
||||
|
||||
// quality
|
||||
final quality = await bind.sessionGetCustomImageQuality(id: id);
|
||||
qualityInitValue =
|
||||
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
|
||||
const qualityMinValue = 10.0;
|
||||
const qualityMaxValue = 100.0;
|
||||
if (qualityInitValue < qualityMinValue) {
|
||||
qualityInitValue = qualityMinValue;
|
||||
}
|
||||
if (qualityInitValue > qualityMaxValue) {
|
||||
qualityInitValue = qualityMaxValue;
|
||||
}
|
||||
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
|
||||
final debouncerQuality = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setCustomValues(quality: v);
|
||||
},
|
||||
initialValue: qualityInitValue,
|
||||
);
|
||||
final qualitySlider = Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Slider(
|
||||
value: qualitySliderValue.value,
|
||||
min: qualityMinValue,
|
||||
max: qualityMaxValue,
|
||||
divisions: 18,
|
||||
onChanged: (double value) {
|
||||
qualitySliderValue.value = value;
|
||||
debouncerQuality.value = value;
|
||||
},
|
||||
)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${qualitySliderValue.value.round()}%',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
translate('Bitrate'),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
],
|
||||
));
|
||||
// fps
|
||||
final fpsOption = await bind.sessionGetOption(id: id, arg: 'custom-fps');
|
||||
fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30;
|
||||
if (fpsInitValue < 5 || fpsInitValue > 120) {
|
||||
fpsInitValue = 30;
|
||||
}
|
||||
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
|
||||
final debouncerFps = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setCustomValues(fps: v);
|
||||
},
|
||||
initialValue: qualityInitValue,
|
||||
);
|
||||
bool? direct;
|
||||
try {
|
||||
direct =
|
||||
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
|
||||
} catch (_) {}
|
||||
final fpsSlider = Offstage(
|
||||
offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Obx((() => Slider(
|
||||
value: fpsSliderValue.value,
|
||||
min: 5,
|
||||
max: 120,
|
||||
divisions: 23,
|
||||
onChanged: (double value) {
|
||||
fpsSliderValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
},
|
||||
)))),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Obx(() => Text(
|
||||
'${fpsSliderValue.value.round()}',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
))),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
translate('FPS'),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final content = Column(
|
||||
children: [qualitySlider, fpsSlider],
|
||||
);
|
||||
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import './dialog.dart';
|
||||
|
||||
class _IconOP extends StatelessWidget {
|
||||
final String icon;
|
||||
@ -324,17 +325,16 @@ class LoginWidgetUserPass extends StatelessWidget {
|
||||
children: [
|
||||
const SizedBox(height: 8.0),
|
||||
DialogTextField(
|
||||
title: translate("Username"),
|
||||
title: translate(DialogTextField.kUsernameTitle),
|
||||
controller: username,
|
||||
focusNode: userFocusNode,
|
||||
prefixIcon: Icon(Icons.account_circle_outlined),
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: usernameMsg),
|
||||
DialogTextField(
|
||||
title: translate("Password"),
|
||||
obscureText: true,
|
||||
controller: pass,
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
errorText: passMsg),
|
||||
PasswordWidget(
|
||||
controller: pass,
|
||||
autoFocus: false,
|
||||
errorText: passMsg,
|
||||
),
|
||||
Obx(() => CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
@ -377,49 +377,6 @@ class LoginWidgetUserPass extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DialogTextField extends StatelessWidget {
|
||||
final String title;
|
||||
final bool obscureText;
|
||||
final String? errorText;
|
||||
final String? helperText;
|
||||
final Widget? prefixIcon;
|
||||
final TextEditingController controller;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
DialogTextField(
|
||||
{Key? key,
|
||||
this.focusNode,
|
||||
this.obscureText = false,
|
||||
this.errorText,
|
||||
this.helperText,
|
||||
this.prefixIcon,
|
||||
required this.title,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: title,
|
||||
prefixIcon: prefixIcon,
|
||||
helperText: helperText,
|
||||
helperMaxLines: 8,
|
||||
errorText: errorText),
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
obscureText: obscureText,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(vertical: 4.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// common login dialog for desktop
|
||||
/// call this directly
|
||||
Future<bool?> loginDialog() async {
|
||||
|
@ -388,6 +388,15 @@ class BlockableOverlayState extends OverlayKeyState {
|
||||
_middleBlocked.value = blocked;
|
||||
}
|
||||
}
|
||||
|
||||
void applyFfi(FFI ffi) {
|
||||
ffi.dialogManager.setOverlayState(this);
|
||||
ffi.chatModel.setOverlayState(this);
|
||||
// make remote page penetrable automatically, effective for chat over remote
|
||||
onMiddleBlockedClick = () {
|
||||
setMiddleBlocked(false);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BlockableOverlay extends StatelessWidget {
|
||||
|
@ -261,7 +261,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
},
|
||||
child: Icon(
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? Icons.list_rounded
|
||||
? Icons.list
|
||||
: Icons.grid_view_rounded,
|
||||
size: 18,
|
||||
color: textColor,
|
||||
@ -455,12 +455,12 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
);
|
||||
|
||||
final translated_text =
|
||||
PeerSortType.values.map((e) => translate(e)).toList();
|
||||
final translated_text = {
|
||||
for (var e in PeerSortType.values) e: translate(e)
|
||||
};
|
||||
|
||||
final double max_width =
|
||||
50 + translated_text.map((e) => e.length).reduce(max) * 10;
|
||||
|
||||
50 + translated_text.values.map((e) => e.length).reduce(max) * 10;
|
||||
return Container(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
decoration: deco,
|
||||
@ -496,20 +496,20 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
||||
),
|
||||
enabled: false,
|
||||
),
|
||||
...translated_text
|
||||
...translated_text.entries
|
||||
.map<DropdownMenuItem<String>>(
|
||||
(String value) => DropdownMenuItem<String>(
|
||||
value: value,
|
||||
(MapEntry entry) => DropdownMenuItem<String>(
|
||||
value: entry.key,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
value == peerSort.value
|
||||
entry.key == peerSort.value
|
||||
? Icons.radio_button_checked_rounded
|
||||
: Icons.radio_button_off_rounded,
|
||||
size: 18,
|
||||
).paddingOnly(right: 12),
|
||||
Text(
|
||||
value,
|
||||
entry.value,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
|
456
flutter/lib/common/widgets/toolbar.dart
Normal file
456
flutter/lib/common/widgets/toolbar.dart
Normal file
@ -0,0 +1,456 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class TTextMenu {
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
Widget? trailingIcon;
|
||||
bool divider;
|
||||
TTextMenu(
|
||||
{required this.child,
|
||||
required this.onPressed,
|
||||
this.trailingIcon,
|
||||
this.divider = false});
|
||||
}
|
||||
|
||||
class TRadioMenu<T> {
|
||||
final Widget child;
|
||||
final T value;
|
||||
final T groupValue;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
TRadioMenu(
|
||||
{required this.child,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged});
|
||||
}
|
||||
|
||||
class TToggleMenu {
|
||||
final Widget child;
|
||||
final bool value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
TToggleMenu(
|
||||
{required this.child, required this.value, required this.onChanged});
|
||||
}
|
||||
|
||||
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
|
||||
List<TTextMenu> v = [];
|
||||
// elevation
|
||||
if (ffi.elevationModel.showRequestMenu) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Request Elevation')),
|
||||
onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)),
|
||||
);
|
||||
}
|
||||
// osAccount / osPassword
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Row(children: [
|
||||
Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
|
||||
Offstage(
|
||||
offstage: isDesktop,
|
||||
child:
|
||||
Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12))
|
||||
]),
|
||||
trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
|
||||
onPressed: () => pi.is_headless
|
||||
? showSetOSAccount(id, ffi.dialogManager)
|
||||
: showSetOSPassword(id, false, ffi.dialogManager)),
|
||||
);
|
||||
// paste
|
||||
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Paste')),
|
||||
onPressed: () async {
|
||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
bind.sessionInputString(id: id, value: data.text ?? "");
|
||||
}
|
||||
}));
|
||||
}
|
||||
// reset canvas
|
||||
if (isMobile) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Reset canvas')),
|
||||
onPressed: () => ffi.cursorModel.reset()));
|
||||
}
|
||||
// transferFile
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer File')),
|
||||
onPressed: () => connect(context, id, isFileTransfer: true)),
|
||||
);
|
||||
}
|
||||
// tcpTunneling
|
||||
if (isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP Tunneling')),
|
||||
onPressed: () => connect(context, id, isTcpTunneling: true)),
|
||||
);
|
||||
}
|
||||
// note
|
||||
if (bind.sessionGetAuditServerSync(id: id, typ: "conn").isNotEmpty) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Note')),
|
||||
onPressed: () => showAuditDialog(id, ffi.dialogManager)),
|
||||
);
|
||||
}
|
||||
// divider
|
||||
if (isDesktop) {
|
||||
v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true));
|
||||
}
|
||||
// ctrlAltDel
|
||||
if (!ffiModel.viewOnly &&
|
||||
ffiModel.keyboard &&
|
||||
(pi.platform == kPeerPlatformLinux || pi.sasEnabled)) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text('${translate("Insert")} Ctrl + Alt + Del'),
|
||||
onPressed: () => bind.sessionCtrlAltDel(id: id)),
|
||||
);
|
||||
}
|
||||
// restart
|
||||
if (perms['restart'] != false &&
|
||||
(pi.platform == kPeerPlatformLinux ||
|
||||
pi.platform == kPeerPlatformWindows ||
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Restart Remote Device')),
|
||||
onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)),
|
||||
);
|
||||
}
|
||||
// insertLock
|
||||
if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Insert Lock')),
|
||||
onPressed: () => bind.sessionLockScreen(id: id)),
|
||||
);
|
||||
}
|
||||
// blockUserInput
|
||||
if (ffi.ffiModel.keyboard &&
|
||||
pi.platform == kPeerPlatformWindows) // privacy-mode != true ??
|
||||
{
|
||||
v.add(TTextMenu(
|
||||
child: Obx(() => Text(translate(
|
||||
'${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))),
|
||||
onPressed: () {
|
||||
RxBool blockInput = BlockInputState.find(id);
|
||||
bind.sessionToggleOption(
|
||||
id: id, value: '${blockInput.value ? 'un' : ''}block-input');
|
||||
blockInput.value = !blockInput.value;
|
||||
}));
|
||||
}
|
||||
// switchSides
|
||||
if (isDesktop &&
|
||||
ffiModel.keyboard &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
pi.platform != kPeerPlatformMacOS &&
|
||||
version_cmp(pi.version, '1.2.0') >= 0) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Switch Sides')),
|
||||
onPressed: () => showConfirmSwitchSidesDialog(id, ffi.dialogManager)));
|
||||
}
|
||||
// refresh
|
||||
if (pi.version.isNotEmpty) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Refresh')),
|
||||
onPressed: () => bind.sessionRefresh(id: id)));
|
||||
}
|
||||
// record
|
||||
var codecFormat = ffi.qualityMonitorModel.data.codecFormat;
|
||||
if (!isDesktop &&
|
||||
(ffi.recordingModel.start ||
|
||||
(perms["recording"] != false &&
|
||||
(codecFormat == "VP8" || codecFormat == "VP9")))) {
|
||||
v.add(TTextMenu(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(translate(ffi.recordingModel.start
|
||||
? 'Stop session recording'
|
||||
: 'Start session recording')),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
child: Icon(
|
||||
ffi.recordingModel.start
|
||||
? Icons.pause_circle_filled
|
||||
: Icons.videocam_outlined,
|
||||
color: MyTheme.accent),
|
||||
)
|
||||
],
|
||||
),
|
||||
onPressed: () => ffi.recordingModel.toggle()));
|
||||
}
|
||||
// fingerprint
|
||||
if (!isDesktop) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('Copy Fingerprint')),
|
||||
onPressed: () => onCopyFingerprint(FingerprintState.find(id).value),
|
||||
));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
Future<List<TRadioMenu<String>>> toolbarViewStyle(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
final groupValue = await bind.sessionGetViewStyle(id: id) ?? '';
|
||||
void onChanged(String? value) async {
|
||||
if (value == null) return;
|
||||
bind
|
||||
.sessionSetViewStyle(id: id, value: value)
|
||||
.then((_) => ffi.canvasModel.updateViewStyle());
|
||||
}
|
||||
|
||||
return [
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Scale original')),
|
||||
value: kRemoteViewStyleOriginal,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Scale adaptive')),
|
||||
value: kRemoteViewStyleAdaptive,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged)
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<TRadioMenu<String>>> toolbarImageQuality(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
final groupValue = await bind.sessionGetImageQuality(id: id) ?? '';
|
||||
onChanged(String? value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionSetImageQuality(id: id, value: value);
|
||||
}
|
||||
|
||||
return [
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Good image quality')),
|
||||
value: kRemoteImageQualityBest,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Balanced')),
|
||||
value: kRemoteImageQualityBalanced,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Optimize reaction time')),
|
||||
value: kRemoteImageQualityLow,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged),
|
||||
TRadioMenu<String>(
|
||||
child: Text(translate('Custom')),
|
||||
value: kRemoteImageQualityCustom,
|
||||
groupValue: groupValue,
|
||||
onChanged: (value) {
|
||||
onChanged(value);
|
||||
customImageQualityDialog(id, ffi);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<TRadioMenu<String>>> toolbarCodec(
|
||||
BuildContext context, String id, FFI ffi) async {
|
||||
final alternativeCodecs = await bind.sessionAlternativeCodecs(id: id);
|
||||
final groupValue =
|
||||
await bind.sessionGetOption(id: id, arg: 'codec-preference') ?? '';
|
||||
final List<bool> codecs = [];
|
||||
try {
|
||||
final Map codecsJson = jsonDecode(alternativeCodecs);
|
||||
final vp8 = codecsJson['vp8'] ?? false;
|
||||
final h264 = codecsJson['h264'] ?? false;
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
codecs.add(vp8);
|
||||
codecs.add(h264);
|
||||
codecs.add(h265);
|
||||
} catch (e) {
|
||||
debugPrint("Show Codec Preference err=$e");
|
||||
}
|
||||
final visible = codecs.length == 3 && (codecs[0] || codecs[1] || codecs[2]);
|
||||
if (!visible) return [];
|
||||
onChanged(String? value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionPeerOption(
|
||||
id: id, name: 'codec-preference', value: value);
|
||||
bind.sessionChangePreferCodec(id: id);
|
||||
}
|
||||
|
||||
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
||||
return TRadioMenu<String>(
|
||||
child: Text(translate(label)),
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: enabled ? onChanged : null);
|
||||
}
|
||||
|
||||
return [
|
||||
radio('Auto', 'auto', true),
|
||||
if (isDesktop || codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
||||
radio('VP9', 'vp9', true),
|
||||
if (isDesktop || codecs[1]) radio('H264', 'h264', codecs[1]),
|
||||
if (isDesktop || codecs[2]) radio('H265', 'h265', codecs[2]),
|
||||
];
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// show remote cursor
|
||||
if (pi.platform != kPeerPlatformAndroid &&
|
||||
!ffi.canvasModel.cursorEmbedded &&
|
||||
!pi.is_wayland) {
|
||||
final state = ShowRemoteCursorState.find(id);
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'show-remote-cursor';
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Show remote cursor')),
|
||||
value: state.value,
|
||||
onChanged: enabled
|
||||
? (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(id: id, value: option);
|
||||
state.value =
|
||||
bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||
}
|
||||
: null));
|
||||
}
|
||||
// zoom cursor
|
||||
final viewStyle = await bind.sessionGetViewStyle(id: id) ?? '';
|
||||
if (!isMobile &&
|
||||
pi.platform != kPeerPlatformAndroid &&
|
||||
viewStyle != kRemoteViewStyleOriginal) {
|
||||
final option = 'zoom-cursor';
|
||||
final peerState = PeerBoolOption.find(id, option);
|
||||
v.add(TToggleMenu(
|
||||
child: Text(translate('Zoom cursor')),
|
||||
value: peerState.value,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(id: id, value: option);
|
||||
peerState.value = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||
},
|
||||
));
|
||||
}
|
||||
// show quality monitor
|
||||
final option = 'show-quality-monitor';
|
||||
v.add(TToggleMenu(
|
||||
value: bind.sessionGetToggleOptionSync(id: id, arg: option),
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await bind.sessionToggleOption(id: id, value: option);
|
||||
ffi.qualityMonitorModel.checkShowQualityMonitor(id);
|
||||
},
|
||||
child: Text(translate('Show quality monitor'))));
|
||||
// mute
|
||||
if (perms['audio'] != false) {
|
||||
final option = 'disable-audio';
|
||||
final value = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(id: id, value: option);
|
||||
},
|
||||
child: Text(translate('Mute'))));
|
||||
}
|
||||
// file copy and paste
|
||||
if (Platform.isWindows &&
|
||||
pi.platform == kPeerPlatformWindows &&
|
||||
perms['file'] != false) {
|
||||
final option = 'enable-file-transfer';
|
||||
final value = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(id: id, value: option);
|
||||
},
|
||||
child: Text(translate('Allow file copy and paste'))));
|
||||
}
|
||||
// disable clipboard
|
||||
if (ffiModel.keyboard && perms['clipboard'] != false) {
|
||||
final enabled = !ffiModel.viewOnly;
|
||||
final option = 'disable-clipboard';
|
||||
var value = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||
if (ffiModel.viewOnly) value = true;
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: enabled
|
||||
? (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(id: id, value: option);
|
||||
}
|
||||
: null,
|
||||
child: Text(translate('Disable clipboard'))));
|
||||
}
|
||||
// lock after session end
|
||||
if (ffiModel.keyboard) {
|
||||
final option = 'lock-after-session-end';
|
||||
final value = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(id: id, value: option);
|
||||
},
|
||||
child: Text(translate('Lock after session end'))));
|
||||
}
|
||||
// privacy mode
|
||||
if (ffiModel.keyboard && pi.features.privacyMode) {
|
||||
final option = 'privacy-mode';
|
||||
final rxValue = PrivacyModeState.find(id);
|
||||
v.add(TToggleMenu(
|
||||
value: rxValue.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
if (ffiModel.pi.currentDisplay != 0) {
|
||||
msgBox(id, 'custom-nook-nocancel-hasclose', 'info',
|
||||
'Please switch to Display 1 first', '', ffi.dialogManager);
|
||||
return;
|
||||
}
|
||||
bind.sessionToggleOption(id: id, value: option);
|
||||
},
|
||||
child: Text(translate('Privacy mode'))));
|
||||
}
|
||||
// swap key
|
||||
if (ffiModel.keyboard &&
|
||||
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
|
||||
(!Platform.isMacOS && pi.platform == kPeerPlatformMacOS))) {
|
||||
final option = 'allow_swap_key';
|
||||
final value = bind.sessionGetToggleOptionSync(id: id, arg: option);
|
||||
v.add(TToggleMenu(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(id: id, value: option);
|
||||
},
|
||||
child: Text(translate('Swap control-command key'))));
|
||||
}
|
||||
return v;
|
||||
}
|
@ -19,6 +19,8 @@ import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/button.dart';
|
||||
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
|
||||
/// Connection page for connecting to a remote peer.
|
||||
class ConnectionPage extends StatefulWidget {
|
||||
const ConnectionPage({Key? key}) : super(key: key);
|
||||
@ -223,9 +225,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
children: [
|
||||
Button(
|
||||
isOutline: true,
|
||||
onTap: () {
|
||||
onConnect(isFileTransfer: true);
|
||||
},
|
||||
onTap: () => onConnect(isFileTransfer: true),
|
||||
text: "Transfer File",
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -301,7 +301,7 @@ class _GeneralState extends State<_General> {
|
||||
|
||||
Widget audio(BuildContext context) {
|
||||
String getDefault() {
|
||||
if (Platform.isWindows) return 'System Sound';
|
||||
if (Platform.isWindows) return translate('System Sound');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -322,7 +322,7 @@ class _GeneralState extends State<_General> {
|
||||
return futureBuilder(future: () async {
|
||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||
if (Platform.isWindows) {
|
||||
devices.insert(0, 'System Sound');
|
||||
devices.insert(0, translate('System Sound'));
|
||||
}
|
||||
String current = await getValue();
|
||||
return {'devices': devices, 'current': current};
|
||||
@ -415,7 +415,7 @@ class _GeneralState extends State<_General> {
|
||||
List<String> keys = langsMap.keys.toList();
|
||||
List<String> values = langsMap.values.toList();
|
||||
keys.insert(0, '');
|
||||
values.insert(0, 'Default');
|
||||
values.insert(0, translate('Default'));
|
||||
String currentKey = data['lang']!;
|
||||
if (!keys.contains(currentKey)) {
|
||||
currentKey = '';
|
||||
@ -1228,9 +1228,9 @@ class _DisplayState extends State<_Display> {
|
||||
children: [
|
||||
Slider(
|
||||
value: fpsValue.value,
|
||||
min: 10.0,
|
||||
min: 5.0,
|
||||
max: 120.0,
|
||||
divisions: 22,
|
||||
divisions: 23,
|
||||
onChanged: (double value) async {
|
||||
fpsValue.value = value;
|
||||
await bind.mainSetUserDefaultOption(
|
||||
@ -1258,9 +1258,6 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
Widget codec(BuildContext context) {
|
||||
if (!bind.mainHasHwcodec()) {
|
||||
return Offstage();
|
||||
}
|
||||
final key = 'codec-preference';
|
||||
onChanged(String value) async {
|
||||
await bind.mainSetUserDefaultOption(key: key, value: value);
|
||||
@ -1268,28 +1265,45 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
final groupValue = bind.mainGetUserDefaultOption(key: key);
|
||||
|
||||
var hwRadios = [];
|
||||
try {
|
||||
final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
|
||||
final h264 = codecsJson['h264'] ?? false;
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
if (h264) {
|
||||
hwRadios.add(_Radio(context,
|
||||
value: 'h264',
|
||||
groupValue: groupValue,
|
||||
label: 'H264',
|
||||
onChanged: onChanged));
|
||||
}
|
||||
if (h265) {
|
||||
hwRadios.add(_Radio(context,
|
||||
value: 'h265',
|
||||
groupValue: groupValue,
|
||||
label: 'H265',
|
||||
onChanged: onChanged));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("failed to parse supported hwdecodings, err=$e");
|
||||
}
|
||||
return _Card(title: 'Default Codec', children: [
|
||||
_Radio(context,
|
||||
value: 'auto',
|
||||
groupValue: groupValue,
|
||||
label: 'Auto',
|
||||
onChanged: onChanged),
|
||||
_Radio(context,
|
||||
value: 'vp8',
|
||||
groupValue: groupValue,
|
||||
label: 'VP8',
|
||||
onChanged: onChanged),
|
||||
_Radio(context,
|
||||
value: 'vp9',
|
||||
groupValue: groupValue,
|
||||
label: 'VP9',
|
||||
onChanged: onChanged),
|
||||
_Radio(context,
|
||||
value: 'h264',
|
||||
groupValue: groupValue,
|
||||
label: 'H264',
|
||||
onChanged: onChanged),
|
||||
_Radio(context,
|
||||
value: 'h265',
|
||||
groupValue: groupValue,
|
||||
label: 'H265',
|
||||
onChanged: onChanged),
|
||||
...hwRadios,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -1376,11 +1390,18 @@ class _AboutState extends State<_About> {
|
||||
final license = await bind.mainGetLicense();
|
||||
final version = await bind.mainGetVersion();
|
||||
final buildDate = await bind.mainGetBuildDate();
|
||||
return {'license': license, 'version': version, 'buildDate': buildDate};
|
||||
final fingerprint = await bind.mainGetFingerprint();
|
||||
return {
|
||||
'license': license,
|
||||
'version': version,
|
||||
'buildDate': buildDate,
|
||||
'fingerprint': fingerprint
|
||||
};
|
||||
}(), hasData: (data) {
|
||||
final license = data['license'].toString();
|
||||
final version = data['version'].toString();
|
||||
final buildDate = data['buildDate'].toString();
|
||||
final fingerprint = data['fingerprint'].toString();
|
||||
const linkStyle = TextStyle(decoration: TextDecoration.underline);
|
||||
final scrollController = ScrollController();
|
||||
return DesktopScrollWrapper(
|
||||
@ -1401,6 +1422,9 @@ class _AboutState extends State<_About> {
|
||||
SelectionArea(
|
||||
child: Text('${translate('Build Date')}: $buildDate')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
SelectionArea(
|
||||
child: Text('${translate('Fingerprint')}: $fingerprint')
|
||||
.marginSymmetric(vertical: 4.0)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrlString('https://rustdesk.com/privacy');
|
||||
|
@ -23,7 +23,7 @@ class DesktopTabPage extends StatefulWidget {
|
||||
DesktopTabController tabController = Get.find();
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelSettingPage,
|
||||
label: translate(kTabLabelSettingPage),
|
||||
label: kTabLabelSettingPage,
|
||||
selectedIcon: Icons.build_sharp,
|
||||
unselectedIcon: Icons.build_outlined,
|
||||
page: DesktopSettingPage(
|
||||
@ -46,7 +46,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
RemoteCountState.init();
|
||||
tabController.add(TabInfo(
|
||||
key: kTabLabelHomePage,
|
||||
label: translate(kTabLabelHomePage),
|
||||
label: kTabLabelHomePage,
|
||||
selectedIcon: Icons.home_sharp,
|
||||
unselectedIcon: Icons.home_outlined,
|
||||
closable: false,
|
||||
|
@ -802,7 +802,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate("Show Hidden Files"),
|
||||
getter: () async {
|
||||
return controller.options.value.isWindows;
|
||||
return controller.options.value.showHidden;
|
||||
},
|
||||
setter: (bool v) async {
|
||||
controller.toggleShowHidden();
|
||||
|
@ -17,7 +17,7 @@ import '../../consts.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
import '../../common/widgets/remote_input.dart';
|
||||
import '../../common.dart';
|
||||
import '../../mobile/widgets/dialog.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
@ -77,15 +77,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
late FFI _ffi;
|
||||
|
||||
void _initStates(String id) {
|
||||
PrivacyModeState.init(id);
|
||||
BlockInputState.init(id);
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
RemoteCursorMovedState.init(id);
|
||||
final optZoomCursor = 'zoom-cursor';
|
||||
PeerBoolOption.init(id, optZoomCursor, () => false);
|
||||
_zoomCursor = PeerBoolOption.find(id, optZoomCursor);
|
||||
initSharedStates(id);
|
||||
_zoomCursor = PeerBoolOption.find(id, 'zoom-cursor');
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||
@ -93,15 +86,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
_textureId = RxInt(-1);
|
||||
}
|
||||
|
||||
void _removeStates(String id) {
|
||||
PrivacyModeState.delete(id);
|
||||
BlockInputState.delete(id);
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
RemoteCursorMovedState.delete(id);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -158,12 +142,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
// _isCustomCursorInited = true;
|
||||
// }
|
||||
|
||||
_ffi.dialogManager.setOverlayState(_blockableOverlayState);
|
||||
_ffi.chatModel.setOverlayState(_blockableOverlayState);
|
||||
// make remote page penetrable automatically, effective for chat over remote
|
||||
_blockableOverlayState.onMiddleBlockedClick = () {
|
||||
_blockableOverlayState.setMiddleBlocked(false);
|
||||
};
|
||||
_blockableOverlayState.applyFfi(_ffi);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -222,7 +201,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
Get.delete<FFI>(tag: widget.id);
|
||||
super.dispose();
|
||||
_removeStates(widget.id);
|
||||
removeSharedStates(widget.id);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
@ -310,7 +289,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
void leaveView(PointerExitEvent evt) {
|
||||
if (_ffi.ffiModel.keyboard()) {
|
||||
if (_ffi.ffiModel.keyboard) {
|
||||
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import 'dart:ui' as ui;
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
@ -158,20 +159,36 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
],
|
||||
);
|
||||
} else {
|
||||
final msgDirect = translate(
|
||||
connectionType.direct.value == ConnectionType.strDirect
|
||||
? 'Direct Connection'
|
||||
: 'Relay Connection');
|
||||
final msgSecure = translate(
|
||||
connectionType.secure.value == ConnectionType.strSecure
|
||||
? 'Secure Connection'
|
||||
: 'Insecure Connection');
|
||||
bool secure =
|
||||
connectionType.secure.value == ConnectionType.strSecure;
|
||||
bool direct =
|
||||
connectionType.direct.value == ConnectionType.strDirect;
|
||||
var msgConn;
|
||||
if (secure && direct) {
|
||||
msgConn = translate("Direct and encrypted connection");
|
||||
} else if (secure && !direct) {
|
||||
msgConn = translate("Relayed and encrypted connection");
|
||||
} else if (!secure && direct) {
|
||||
msgConn = translate("Direct and unencrypted connection");
|
||||
} else {
|
||||
msgConn = translate("Relayed and unencrypted connection");
|
||||
}
|
||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||
var fingerprint = FingerprintState.find(key).value;
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
Tooltip(
|
||||
message: '$msgDirect\n$msgSecure',
|
||||
message: '$msgConn\n$msgFingerprint',
|
||||
child: SvgPicture.asset(
|
||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
||||
width: themeConf.iconSize,
|
||||
@ -259,7 +276,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
),
|
||||
]);
|
||||
|
||||
if (!ffi.canvasModel.cursorEmbedded && !ffi.ffiModel.viewOnly) {
|
||||
if (!ffi.canvasModel.cursorEmbedded &&
|
||||
!ffi.ffiModel.viewOnly &&
|
||||
!pi.is_wayland) {
|
||||
menu.add(MenuEntryDivider<String>());
|
||||
menu.add(RemoteMenuEntry.showRemoteCursor(
|
||||
key,
|
||||
@ -283,6 +302,17 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
menu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Copy Fingerprint'),
|
||||
style: style,
|
||||
),
|
||||
proc: () => onCopyFingerprint(FingerprintState.find(key).value),
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
dismissCallback: cancelFunc,
|
||||
));
|
||||
|
||||
return mod_menu.PopupMenu<String>(
|
||||
items: menu
|
||||
.map((entry) => entry.build(
|
||||
|
49
flutter/lib/desktop/plugin/common.dart
Normal file
49
flutter/lib/desktop/plugin/common.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'dart:convert';
|
||||
|
||||
typedef PluginId = String;
|
||||
|
||||
// ui location
|
||||
const String kLocationHostMainDisplayOthers =
|
||||
'host|main|settings|display|others';
|
||||
const String kLocationClientRemoteToolbarDisplay =
|
||||
'client|remote|toolbar|display';
|
||||
|
||||
class MsgFromUi {
|
||||
String remotePeerId;
|
||||
String localPeerId;
|
||||
String id;
|
||||
String name;
|
||||
String location;
|
||||
String key;
|
||||
String value;
|
||||
String action;
|
||||
|
||||
MsgFromUi({
|
||||
required this.remotePeerId,
|
||||
required this.localPeerId,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.location,
|
||||
required this.key,
|
||||
required this.value,
|
||||
required this.action,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'remote_peer_id': remotePeerId,
|
||||
'local_peer_id': localPeerId,
|
||||
'id': id,
|
||||
'name': name,
|
||||
'location': location,
|
||||
'key': key,
|
||||
'value': value,
|
||||
'action': action,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode(toJson());
|
||||
}
|
||||
}
|
166
flutter/lib/desktop/plugin/desc.dart
Normal file
166
flutter/lib/desktop/plugin/desc.dart
Normal file
@ -0,0 +1,166 @@
|
||||
import 'dart:collection';
|
||||
|
||||
const String kValueTrue = '1';
|
||||
const String kValueFalse = '0';
|
||||
|
||||
class UiType {
|
||||
String key;
|
||||
String text;
|
||||
String tooltip;
|
||||
String action;
|
||||
|
||||
UiType(this.key, this.text, this.tooltip, this.action);
|
||||
|
||||
UiType.fromJson(Map<String, dynamic> json)
|
||||
: key = json['key'] ?? '',
|
||||
text = json['text'] ?? '',
|
||||
tooltip = json['tooltip'] ?? '',
|
||||
action = json['action'] ?? '';
|
||||
|
||||
static UiType? create(Map<String, dynamic> json) {
|
||||
if (json['t'] == 'Button') {
|
||||
return UiButton.fromJson(json['c']);
|
||||
} else if (json['t'] == 'Checkbox') {
|
||||
return UiCheckbox.fromJson(json['c']);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UiButton extends UiType {
|
||||
String icon;
|
||||
|
||||
UiButton(
|
||||
{required String key,
|
||||
required String text,
|
||||
required this.icon,
|
||||
required String tooltip,
|
||||
required String action})
|
||||
: super(key, text, tooltip, action);
|
||||
|
||||
UiButton.fromJson(Map<String, dynamic> json)
|
||||
: icon = json['icon'] ?? '',
|
||||
super.fromJson(json);
|
||||
}
|
||||
|
||||
class UiCheckbox extends UiType {
|
||||
UiCheckbox(
|
||||
{required String key,
|
||||
required String text,
|
||||
required String tooltip,
|
||||
required String action})
|
||||
: super(key, text, tooltip, action);
|
||||
|
||||
UiCheckbox.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
}
|
||||
|
||||
class Location {
|
||||
// location key:
|
||||
// host|main|settings|display|others
|
||||
// client|remote|toolbar|display
|
||||
HashMap<String, UiType> ui;
|
||||
|
||||
Location(this.ui);
|
||||
Location.fromJson(Map<String, dynamic> json) : ui = HashMap() {
|
||||
json.forEach((key, value) {
|
||||
var ui = UiType.create(value);
|
||||
if (ui != null) {
|
||||
this.ui[ui.key] = ui;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigItem {
|
||||
String key;
|
||||
String value;
|
||||
String description;
|
||||
String defaultValue;
|
||||
|
||||
ConfigItem(this.key, this.value, this.defaultValue, this.description);
|
||||
ConfigItem.fromJson(Map<String, dynamic> json)
|
||||
: key = json['key'] ?? '',
|
||||
value = json['value'] ?? '',
|
||||
description = json['description'] ?? '',
|
||||
defaultValue = json['default'] ?? '';
|
||||
|
||||
static String get trueValue => kValueTrue;
|
||||
static String get falseValue => kValueFalse;
|
||||
static bool isTrue(String value) => value == kValueTrue;
|
||||
static bool isFalse(String value) => value == kValueFalse;
|
||||
}
|
||||
|
||||
class Config {
|
||||
List<ConfigItem> local;
|
||||
List<ConfigItem> peer;
|
||||
|
||||
Config(this.local, this.peer);
|
||||
Config.fromJson(Map<String, dynamic> json)
|
||||
: local = (json['local'] as List<dynamic>)
|
||||
.map((e) => ConfigItem.fromJson(e))
|
||||
.toList(),
|
||||
peer = (json['peer'] as List<dynamic>)
|
||||
.map((e) => ConfigItem.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
class Desc {
|
||||
String id;
|
||||
String name;
|
||||
String version;
|
||||
String description;
|
||||
String author;
|
||||
String home;
|
||||
String license;
|
||||
String published;
|
||||
String released;
|
||||
String github;
|
||||
Location location;
|
||||
Config config;
|
||||
|
||||
Desc(
|
||||
this.id,
|
||||
this.name,
|
||||
this.version,
|
||||
this.description,
|
||||
this.author,
|
||||
this.home,
|
||||
this.license,
|
||||
this.published,
|
||||
this.released,
|
||||
this.github,
|
||||
this.location,
|
||||
this.config);
|
||||
|
||||
Desc.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'] ?? '',
|
||||
name = json['name'] ?? '',
|
||||
version = json['version'] ?? '',
|
||||
description = json['description'] ?? '',
|
||||
author = json['author'] ?? '',
|
||||
home = json['home'] ?? '',
|
||||
license = json['license'] ?? '',
|
||||
published = json['published'] ?? '',
|
||||
released = json['released'] ?? '',
|
||||
github = json['github'] ?? '',
|
||||
location = Location(HashMap<String, UiType>.from(json['location'])),
|
||||
config = Config(
|
||||
(json['config'] as List<dynamic>)
|
||||
.map((e) => ConfigItem.fromJson(e))
|
||||
.toList(),
|
||||
(json['config'] as List<dynamic>)
|
||||
.map((e) => ConfigItem.fromJson(e))
|
||||
.toList());
|
||||
}
|
||||
|
||||
final mapPluginDesc = <String, Desc>{};
|
||||
|
||||
void updateDesc(Map<String, dynamic> desc) {
|
||||
Desc d = Desc.fromJson(desc);
|
||||
mapPluginDesc[d.id] = d;
|
||||
}
|
||||
|
||||
Desc? getDesc(String id) {
|
||||
return mapPluginDesc[id];
|
||||
}
|
60
flutter/lib/desktop/plugin/event.dart
Normal file
60
flutter/lib/desktop/plugin/event.dart
Normal file
@ -0,0 +1,60 @@
|
||||
void handlePluginEvent(
|
||||
Map<String, dynamic> evt,
|
||||
String peer,
|
||||
Function(Map<String, dynamic> e) handleMsgBox,
|
||||
) {
|
||||
// content
|
||||
//
|
||||
// {
|
||||
// "t": "Option",
|
||||
// "c": {
|
||||
// "id": "id from RustDesk platform",
|
||||
// "name": "Privacy Mode",
|
||||
// "version": "v0.1.0",
|
||||
// "location": "client|remote|toolbar|display",
|
||||
// "key": "privacy-mode",
|
||||
// "value": "1"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// {
|
||||
// "t": "MsgBox",
|
||||
// "c": {
|
||||
// "type": "custom-nocancel",
|
||||
// "title": "Privacy Mode",
|
||||
// "text": "Failed unknown",
|
||||
// "link": ""
|
||||
// }
|
||||
// }
|
||||
//
|
||||
if (evt['content']?['c'] == null) return;
|
||||
final t = evt['content']?['t'];
|
||||
if (t == 'Option') {
|
||||
handleOptionEvent(evt['content']?['c'], peer);
|
||||
} else if (t == 'MsgBox') {
|
||||
handleMsgBox(evt['content']?['c']);
|
||||
}
|
||||
}
|
||||
|
||||
void handleOptionEvent(Map<String, dynamic> evt, String peer) {
|
||||
// content
|
||||
//
|
||||
// {
|
||||
// "id": "id from RustDesk platform",
|
||||
// "name": "Privacy Mode",
|
||||
// "version": "v0.1.0",
|
||||
// "location": "client|remote|toolbar|display",
|
||||
// "key": "privacy-mode",
|
||||
// "value": "1"
|
||||
// }
|
||||
//
|
||||
final key = evt['key'];
|
||||
final value = evt['value'];
|
||||
if (key == 'privacy-mode') {
|
||||
if (value == '1') {
|
||||
// enable privacy mode
|
||||
} else {
|
||||
// disable privacy mode
|
||||
}
|
||||
}
|
||||
}
|
44
flutter/lib/desktop/plugin/model.dart
Normal file
44
flutter/lib/desktop/plugin/model.dart
Normal file
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import './common.dart';
|
||||
import './desc.dart';
|
||||
|
||||
final Map<String, LocationModel> locationModels = {};
|
||||
|
||||
class PluginModel with ChangeNotifier {
|
||||
final List<UiType> uiList = [];
|
||||
|
||||
void add(UiType ui) {
|
||||
uiList.add(ui);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get isEmpty => uiList.isEmpty;
|
||||
}
|
||||
|
||||
class LocationModel with ChangeNotifier {
|
||||
final Map<PluginId, PluginModel> pluginModels = {};
|
||||
|
||||
void add(PluginId id, UiType ui) {
|
||||
if (pluginModels[id] != null) {
|
||||
pluginModels[id]!.add(ui);
|
||||
} else {
|
||||
var model = PluginModel();
|
||||
model.add(ui);
|
||||
pluginModels[id] = model;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get isEmpty => pluginModels.isEmpty;
|
||||
}
|
||||
|
||||
void addLocationUi(String location, PluginId id, UiType ui) {
|
||||
locationModels[location]?.add(id, ui);
|
||||
}
|
||||
|
||||
LocationModel addLocation(String location) {
|
||||
if (locationModels[location] == null) {
|
||||
locationModels[location] = LocationModel();
|
||||
}
|
||||
return locationModels[location]!;
|
||||
}
|
175
flutter/lib/desktop/plugin/widget.dart
Normal file
175
flutter/lib/desktop/plugin/widget.dart
Normal file
@ -0,0 +1,175 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
import './desc.dart';
|
||||
import './model.dart';
|
||||
import './common.dart';
|
||||
|
||||
class LocationItem extends StatelessWidget {
|
||||
final String peerId;
|
||||
final FFI ffi;
|
||||
final String location;
|
||||
final LocationModel locationModel;
|
||||
|
||||
LocationItem({
|
||||
Key? key,
|
||||
required this.peerId,
|
||||
required this.ffi,
|
||||
required this.location,
|
||||
required this.locationModel,
|
||||
}) : super(key: key);
|
||||
|
||||
bool get isEmpty => locationModel.isEmpty;
|
||||
|
||||
static LocationItem createLocationItem(
|
||||
String peerId, FFI ffi, String location) {
|
||||
final model = addLocation(location);
|
||||
return LocationItem(
|
||||
peerId: peerId,
|
||||
ffi: ffi,
|
||||
location: location,
|
||||
locationModel: model,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: locationModel,
|
||||
child: Consumer<LocationModel>(builder: (context, model, child) {
|
||||
return Column(
|
||||
children: model.pluginModels.entries
|
||||
.map((entry) => _buildPluginItem(entry.key, entry.value))
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPluginItem(PluginId id, PluginModel model) => PluginItem(
|
||||
pluginId: id,
|
||||
peerId: peerId,
|
||||
ffi: ffi,
|
||||
location: location,
|
||||
pluginModel: model,
|
||||
);
|
||||
}
|
||||
|
||||
class PluginItem extends StatelessWidget {
|
||||
final PluginId pluginId;
|
||||
final String peerId;
|
||||
final FFI ffi;
|
||||
final String location;
|
||||
final PluginModel pluginModel;
|
||||
|
||||
PluginItem({
|
||||
Key? key,
|
||||
required this.pluginId,
|
||||
required this.peerId,
|
||||
required this.ffi,
|
||||
required this.location,
|
||||
required this.pluginModel,
|
||||
}) : super(key: key);
|
||||
|
||||
bool get isEmpty => pluginModel.isEmpty;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: pluginModel,
|
||||
child: Consumer<PluginModel>(builder: (context, model, child) {
|
||||
return Column(
|
||||
children: model.uiList.map((ui) => _buildItem(ui)).toList(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// to-do: add plugin icon and tooltip
|
||||
Widget _buildItem(UiType ui) {
|
||||
switch (ui.runtimeType) {
|
||||
case UiButton:
|
||||
return _buildMenuButton(ui as UiButton);
|
||||
case UiCheckbox:
|
||||
return _buildCheckboxMenuButton(ui as UiCheckbox);
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List _makeEvent(
|
||||
String localPeerId,
|
||||
String key, {
|
||||
bool? v,
|
||||
}) {
|
||||
final event = MsgFromUi(
|
||||
remotePeerId: peerId,
|
||||
localPeerId: localPeerId,
|
||||
id: pluginId,
|
||||
name: getDesc(pluginId)?.name ?? '',
|
||||
location: location,
|
||||
key: key,
|
||||
value:
|
||||
v != null ? (v ? ConfigItem.trueValue : ConfigItem.falseValue) : '',
|
||||
action: '',
|
||||
);
|
||||
return Uint8List.fromList(event.toString().codeUnits);
|
||||
}
|
||||
|
||||
Widget _buildMenuButton(UiButton ui) {
|
||||
return MenuButton(
|
||||
onPressed: () {
|
||||
() async {
|
||||
final localPeerId = await bind.mainGetMyId();
|
||||
bind.pluginEvent(
|
||||
id: pluginId,
|
||||
event: _makeEvent(localPeerId, ui.key),
|
||||
);
|
||||
}();
|
||||
},
|
||||
trailingIcon: Icon(
|
||||
IconData(int.parse(ui.icon, radix: 16), fontFamily: 'MaterialIcons')),
|
||||
// to-do: RustDesk translate or plugin translate ?
|
||||
child: Text(ui.text),
|
||||
ffi: ffi,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckboxMenuButton(UiCheckbox ui) {
|
||||
final v =
|
||||
bind.pluginGetSessionOption(id: pluginId, peer: peerId, key: ui.key);
|
||||
if (v == null) {
|
||||
// session or plugin not found
|
||||
return Container();
|
||||
}
|
||||
return CkbMenuButton(
|
||||
value: ConfigItem.isTrue(v),
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
() async {
|
||||
final localPeerId = await bind.mainGetMyId();
|
||||
bind.pluginEvent(
|
||||
id: pluginId,
|
||||
event: _makeEvent(localPeerId, ui.key, v: v),
|
||||
);
|
||||
}();
|
||||
}
|
||||
},
|
||||
// to-do: rustdesk translate or plugin translate ?
|
||||
child: Text(ui.text),
|
||||
ffi: ffi,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void handleReloading(Map<String, dynamic> evt, String peer) {
|
||||
if (evt['id'] == null || evt['location'] == null) {
|
||||
return;
|
||||
}
|
||||
final ui = UiType.fromJson(evt);
|
||||
addLocationUi(evt['location']!, evt['id']!, ui);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -869,7 +869,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
|
||||
child: Text(
|
||||
widget.label.value,
|
||||
translate(widget.label.value),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
|
@ -134,10 +134,8 @@ void runMainApp(bool startService) async {
|
||||
await restoreWindowPosition(WindowType.Main);
|
||||
// Check the startup argument, if we successfully handle the argument, we keep the main window hidden.
|
||||
final handledByUniLinks = await initUniLinks();
|
||||
final handledByCli = checkArguments();
|
||||
debugPrint(
|
||||
"handled by uni links: $handledByUniLinks, handled by cli: $handledByCli");
|
||||
if (handledByUniLinks || handledByCli) {
|
||||
debugPrint("handled by uni links: $handledByUniLinks");
|
||||
if (handledByUniLinks || checkArguments()) {
|
||||
windowManager.hide();
|
||||
} else {
|
||||
windowManager.show();
|
||||
@ -250,6 +248,7 @@ void hideCmWindow() {
|
||||
windowManager.setOpacity(0);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
bind.mainHideDocker();
|
||||
await windowManager.minimize();
|
||||
await windowManager.hide();
|
||||
});
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import 'package:toggle_switch/toggle_switch.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
FileManagerPage({Key? key, required this.id}) : super(key: key);
|
||||
|
@ -4,22 +4,25 @@ import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/remote_input.dart';
|
||||
import '../../models/input_model.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../utils/image.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
import '../widgets/gestures.dart';
|
||||
|
||||
final initText = '\1' * 1024;
|
||||
@ -42,6 +45,8 @@ class _RemotePageState extends State<RemotePage> {
|
||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||
Orientation? _currentOrientation;
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
final keyboardVisibilityController = KeyboardVisibilityController();
|
||||
late final StreamSubscription<bool> keyboardSubscription;
|
||||
final FocusNode _mobileFocusNode = FocusNode();
|
||||
@ -66,6 +71,8 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||
keyboardSubscription =
|
||||
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
||||
_blockableOverlayState.applyFfi(gFFI);
|
||||
initSharedStates(widget.id);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -82,6 +89,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
overlays: SystemUiOverlay.values);
|
||||
Wakelock.disable();
|
||||
keyboardSubscription.cancel();
|
||||
removeSharedStates(widget.id);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -540,128 +548,21 @@ class _RemotePageState extends State<RemotePage> {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final x = 120.0;
|
||||
final y = size.height;
|
||||
final more = <PopupMenuItem<String>>[];
|
||||
final pi = gFFI.ffiModel.pi;
|
||||
final perms = gFFI.ffiModel.permissions;
|
||||
if (pi.version.isNotEmpty) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Refresh')), value: 'refresh'));
|
||||
}
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Row(
|
||||
children: ([
|
||||
Text(translate('OS Password')),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
showSetOSPassword(id, false, gFFI.dialogManager);
|
||||
},
|
||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
||||
)
|
||||
])),
|
||||
value: 'enter_os_password'));
|
||||
if (!isWebDesktop) {
|
||||
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Paste')), value: 'paste'));
|
||||
}
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
|
||||
}
|
||||
if (perms['keyboard'] != false) {
|
||||
// * Currently mobile does not enable map mode
|
||||
// more.add(PopupMenuItem<String>(
|
||||
// child: Text(translate('Physical Keyboard Input Mode')),
|
||||
// value: 'input-mode'));
|
||||
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text('${translate('Insert')} Ctrl + Alt + Del'),
|
||||
value: 'cad'));
|
||||
}
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Insert Lock')), value: 'lock'));
|
||||
if (pi.platform == kPeerPlatformWindows &&
|
||||
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
|
||||
true) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate(
|
||||
'${gFFI.ffiModel.inputBlocked ? 'Unb' : 'B'}lock user input')),
|
||||
value: 'block-input'));
|
||||
}
|
||||
}
|
||||
if (perms["restart"] != false &&
|
||||
(pi.platform == kPeerPlatformLinux ||
|
||||
pi.platform == kPeerPlatformWindows ||
|
||||
pi.platform == kPeerPlatformMacOS)) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Restart Remote Device')), value: 'restart'));
|
||||
}
|
||||
// Currently only support VP9
|
||||
if (gFFI.recordingModel.start ||
|
||||
(perms["recording"] != false &&
|
||||
gFFI.qualityMonitorModel.data.codecFormat == "VP9")) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(translate(gFFI.recordingModel.start
|
||||
? 'Stop session recording'
|
||||
: 'Start session recording')),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
child: Icon(
|
||||
gFFI.recordingModel.start
|
||||
? Icons.pause_circle_filled
|
||||
: Icons.videocam_outlined,
|
||||
color: MyTheme.accent),
|
||||
)
|
||||
],
|
||||
),
|
||||
value: 'record'));
|
||||
}
|
||||
final menus = toolbarControls(context, id, gFFI);
|
||||
final more = menus
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => PopupMenuItem<int>(child: e.value.child, value: e.key))
|
||||
.toList();
|
||||
() async {
|
||||
var value = await showMenu(
|
||||
var index = await showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||
items: more,
|
||||
elevation: 8,
|
||||
);
|
||||
if (value == 'cad') {
|
||||
bind.sessionCtrlAltDel(id: widget.id);
|
||||
// * Currently mobile does not enable map mode
|
||||
// } else if (value == 'input-mode') {
|
||||
// changePhysicalKeyboardInputMode();
|
||||
} else if (value == 'lock') {
|
||||
bind.sessionLockScreen(id: widget.id);
|
||||
} else if (value == 'block-input') {
|
||||
bind.sessionToggleOption(
|
||||
id: widget.id,
|
||||
value: '${gFFI.ffiModel.inputBlocked ? 'un' : ''}block-input');
|
||||
gFFI.ffiModel.inputBlocked = !gFFI.ffiModel.inputBlocked;
|
||||
} else if (value == 'refresh') {
|
||||
bind.sessionRefresh(id: widget.id);
|
||||
} else if (value == 'paste') {
|
||||
() async {
|
||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
bind.sessionInputString(id: widget.id, value: data.text ?? "");
|
||||
}
|
||||
}();
|
||||
} else if (value == 'enter_os_password') {
|
||||
// FIXME:
|
||||
// null means no session of id
|
||||
// empty string means no password
|
||||
var password = await bind.sessionGetOption(id: id, arg: 'os-password');
|
||||
if (password != null) {
|
||||
bind.sessionInputOsPassword(id: widget.id, value: password);
|
||||
} else {
|
||||
showSetOSPassword(id, true, gFFI.dialogManager);
|
||||
}
|
||||
} else if (value == 'reset_canvas') {
|
||||
gFFI.cursorModel.reset();
|
||||
} else if (value == 'restart') {
|
||||
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
|
||||
} else if (value == 'record') {
|
||||
gFFI.recordingModel.toggle();
|
||||
if (index != null && index < menus.length) {
|
||||
menus[index].onPressed.call();
|
||||
}
|
||||
}();
|
||||
}
|
||||
@ -695,10 +596,8 @@ class _RemotePageState extends State<RemotePage> {
|
||||
// return CustomAlertDialog(
|
||||
// title: Text(translate('Physical Keyboard Input Mode')),
|
||||
// content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// getRadio('Legacy mode', 'legacy', current, setMode,
|
||||
// contentPadding: EdgeInsets.zero),
|
||||
// getRadio('Map mode', 'map', current, setMode,
|
||||
// contentPadding: EdgeInsets.zero),
|
||||
// getRadio('Legacy mode', 'legacy', current, setMode),
|
||||
// getRadio('Map mode', 'map', current, setMode),
|
||||
// ]));
|
||||
// }, clickMaskDismiss: true);
|
||||
// }
|
||||
@ -918,14 +817,6 @@ class CursorPaint extends StatelessWidget {
|
||||
|
||||
void showOptions(
|
||||
BuildContext context, String id, OverlayDialogManager dialogManager) async {
|
||||
String quality =
|
||||
await bind.sessionGetImageQuality(id: id) ?? kRemoteImageQualityBalanced;
|
||||
if (quality == '') quality = kRemoteImageQualityBalanced;
|
||||
String codec =
|
||||
await bind.sessionGetOption(id: id, arg: 'codec-preference') ?? 'auto';
|
||||
if (codec == '') codec = 'auto';
|
||||
String viewStyle = await bind.sessionGetViewStyle(id: id) ?? '';
|
||||
|
||||
var displays = <Widget>[];
|
||||
final pi = gFFI.ffiModel.pi;
|
||||
final image = gFFI.ffiModel.getConnectionImage();
|
||||
@ -968,155 +859,65 @@ void showOptions(
|
||||
if (displays.isNotEmpty) {
|
||||
displays.add(const Divider(color: MyTheme.border));
|
||||
}
|
||||
final perms = gFFI.ffiModel.permissions;
|
||||
final hasHwcodec = bind.mainHasHwcodec();
|
||||
final List<bool> codecs = [];
|
||||
if (hasHwcodec) {
|
||||
try {
|
||||
final Map codecsJson =
|
||||
jsonDecode(await bind.sessionSupportedHwcodec(id: id));
|
||||
final h264 = codecsJson['h264'] ?? false;
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
codecs.add(h264);
|
||||
codecs.add(h265);
|
||||
} catch (e) {
|
||||
debugPrint("Show Codec Preference err=$e");
|
||||
}
|
||||
}
|
||||
|
||||
List<TRadioMenu<String>> viewStyleRadios =
|
||||
await toolbarViewStyle(context, id, gFFI);
|
||||
List<TRadioMenu<String>> imageQualityRadios =
|
||||
await toolbarImageQuality(context, id, gFFI);
|
||||
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
|
||||
List<TToggleMenu> displayToggles =
|
||||
await toolbarDisplayToggle(context, id, gFFI);
|
||||
|
||||
dialogManager.show((setState, close) {
|
||||
final more = <Widget>[];
|
||||
if (perms['audio'] != false) {
|
||||
more.add(getToggle(id, setState, 'disable-audio', 'Mute'));
|
||||
}
|
||||
if (perms['keyboard'] != false) {
|
||||
if (perms['clipboard'] != false) {
|
||||
more.add(
|
||||
getToggle(id, setState, 'disable-clipboard', 'Disable clipboard'));
|
||||
}
|
||||
more.add(getToggle(
|
||||
id, setState, 'lock-after-session-end', 'Lock after session end'));
|
||||
if (pi.platform == kPeerPlatformWindows) {
|
||||
more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode'));
|
||||
}
|
||||
}
|
||||
setQuality(String? value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
quality = value;
|
||||
bind.sessionSetImageQuality(id: id, value: value);
|
||||
});
|
||||
}
|
||||
|
||||
setViewStyle(String? value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
viewStyle = value;
|
||||
bind
|
||||
.sessionSetViewStyle(id: id, value: value)
|
||||
.then((_) => gFFI.canvasModel.updateViewStyle());
|
||||
});
|
||||
}
|
||||
|
||||
setCodec(String? value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
codec = value;
|
||||
bind
|
||||
.sessionPeerOption(id: id, name: "codec-preference", value: value)
|
||||
.then((_) => bind.sessionChangePreferCodec(id: id));
|
||||
});
|
||||
}
|
||||
|
||||
var viewStyle =
|
||||
(viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
|
||||
var imageQuality =
|
||||
(imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '')
|
||||
.obs;
|
||||
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
||||
final radios = [
|
||||
getRadio(
|
||||
'Scale original', kRemoteViewStyleOriginal, viewStyle, setViewStyle),
|
||||
getRadio(
|
||||
'Scale adaptive', kRemoteViewStyleAdaptive, viewStyle, setViewStyle),
|
||||
for (var e in viewStyleRadios)
|
||||
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) viewStyle.value = v;
|
||||
})),
|
||||
const Divider(color: MyTheme.border),
|
||||
getRadio(
|
||||
'Good image quality', kRemoteImageQualityBest, quality, setQuality),
|
||||
getRadio('Balanced', kRemoteImageQualityBalanced, quality, setQuality),
|
||||
getRadio('Optimize reaction time', kRemoteImageQualityLow, quality,
|
||||
setQuality),
|
||||
const Divider(color: MyTheme.border)
|
||||
for (var e in imageQualityRadios)
|
||||
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) imageQuality.value = v;
|
||||
})),
|
||||
const Divider(color: MyTheme.border),
|
||||
for (var e in codecRadios)
|
||||
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) codec.value = v;
|
||||
})),
|
||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||
];
|
||||
|
||||
if (hasHwcodec && codecs.length == 2 && (codecs[0] || codecs[1])) {
|
||||
radios.addAll([
|
||||
getRadio(translate('Auto'), 'auto', codec, setCodec),
|
||||
getRadio('VP9', 'vp9', codec, setCodec),
|
||||
]);
|
||||
if (codecs[0]) {
|
||||
radios.add(getRadio('H264', 'h264', codec, setCodec));
|
||||
}
|
||||
if (codecs[1]) {
|
||||
radios.add(getRadio('H265', 'h265', codec, setCodec));
|
||||
}
|
||||
radios.add(const Divider(color: MyTheme.border));
|
||||
}
|
||||
|
||||
final toggles = [
|
||||
getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'),
|
||||
];
|
||||
if (!gFFI.canvasModel.cursorEmbedded) {
|
||||
toggles.insert(0,
|
||||
getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'));
|
||||
}
|
||||
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
|
||||
final toggles = displayToggles
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => Obx(() => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxToggleValues[e.key].value,
|
||||
onChanged: (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxToggleValues[e.key].value = v;
|
||||
},
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: displays + radios + toggles + more),
|
||||
contentPadding: 0,
|
||||
children: displays + radios + toggles),
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}
|
||||
|
||||
void showSetOSPassword(
|
||||
String id, bool login, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('OS Password')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton(
|
||||
'OK',
|
||||
onPressed: () {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: "os-password", value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
|
||||
if (text != "" && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
},
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
void sendPrompt(bool isMac, String key) {
|
||||
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
||||
if (isMac) {
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
@ -45,6 +46,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _autoRecordIncomingSession = false;
|
||||
var _localIP = "";
|
||||
var _directAccessPort = "";
|
||||
var _fingerprint = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -135,6 +137,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_directAccessPort = directAccessPort;
|
||||
}
|
||||
|
||||
final fingerprint = await bind.mainGetFingerprint();
|
||||
if (_fingerprint != fingerprint) {
|
||||
update = true;
|
||||
_fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
if (update) {
|
||||
setState(() {});
|
||||
}
|
||||
@ -462,6 +470,14 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
)),
|
||||
),
|
||||
leading: Icon(Icons.info)),
|
||||
SettingsTile.navigation(
|
||||
onPressed: (context) => onCopyFingerprint(_fingerprint),
|
||||
title: Text(translate("Fingerprint")),
|
||||
value: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(_fingerprint),
|
||||
),
|
||||
leading: Icon(Icons.fingerprint)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -502,19 +518,18 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: SizedBox.shrink(),
|
||||
content: Column(
|
||||
children: [
|
||||
getRadio('Default', '', lang, setLang),
|
||||
Divider(color: MyTheme.border),
|
||||
] +
|
||||
langs.map((e) {
|
||||
final key = e[0] as String;
|
||||
final name = e[1] as String;
|
||||
return getRadio(name, key, lang, setLang);
|
||||
}).toList(),
|
||||
),
|
||||
actions: []);
|
||||
content: Column(
|
||||
children: [
|
||||
getRadio(Text(translate('Default')), '', lang, setLang),
|
||||
Divider(color: MyTheme.border),
|
||||
] +
|
||||
langs.map((e) {
|
||||
final key = e[0] as String;
|
||||
final name = e[1] as String;
|
||||
return getRadio(Text(translate(name)), key, lang, setLang);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
} catch (e) {
|
||||
//
|
||||
@ -536,14 +551,14 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: SizedBox.shrink(),
|
||||
contentPadding: 10,
|
||||
content: Column(children: [
|
||||
getRadio('Light', ThemeMode.light, themeMode, setTheme),
|
||||
getRadio('Dark', ThemeMode.dark, themeMode, setTheme),
|
||||
getRadio('Follow System', ThemeMode.system, themeMode, setTheme)
|
||||
]),
|
||||
actions: []);
|
||||
content: Column(children: [
|
||||
getRadio(
|
||||
Text(translate('Light')), ThemeMode.light, themeMode, setTheme),
|
||||
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode, setTheme),
|
||||
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
||||
setTheme)
|
||||
]),
|
||||
);
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
|
@ -4,51 +4,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
|
||||
void clientClose(String id, OverlayDialogManager dialogManager) {
|
||||
msgBox(id, 'info', 'Close', 'Are you sure to close the connection?', '',
|
||||
dialogManager);
|
||||
}
|
||||
|
||||
void showSuccess() {
|
||||
void _showSuccess() {
|
||||
showToast(translate("Successful"));
|
||||
}
|
||||
|
||||
void showError() {
|
||||
void _showError() {
|
||||
showToast(translate("Error"));
|
||||
}
|
||||
|
||||
void showRestartRemoteDevice(
|
||||
PeerInfo pi, String id, OverlayDialogManager dialogManager) async {
|
||||
final res =
|
||||
await dialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
|
||||
Text(translate("Restart Remote Device")).paddingOnly(left: 10),
|
||||
]),
|
||||
content: Text(
|
||||
"${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: () => close(true),
|
||||
),
|
||||
],
|
||||
onCancel: close,
|
||||
onSubmit: () => close(true),
|
||||
));
|
||||
if (res == true) bind.sessionRestartRemoteDevice(id: id);
|
||||
}
|
||||
|
||||
void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
||||
final pw = await bind.mainGetPermanentPassword();
|
||||
final p0 = TextEditingController(text: pw);
|
||||
@ -61,10 +26,10 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
||||
dialogManager.showLoading(translate("Waiting"));
|
||||
if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
|
||||
dialogManager.dismissAll();
|
||||
showSuccess();
|
||||
_showSuccess();
|
||||
} else {
|
||||
dialogManager.dismissAll();
|
||||
showError();
|
||||
_showError();
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,117 +122,29 @@ void setTemporaryPasswordLengthDialog(
|
||||
bind.mainUpdateTemporaryPassword();
|
||||
Future.delayed(Duration(milliseconds: 200), () {
|
||||
close();
|
||||
showSuccess();
|
||||
_showSuccess();
|
||||
});
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set one-time password length")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children:
|
||||
lengths.map((e) => getRadio(e, e, length, setLength)).toList()),
|
||||
actions: [],
|
||||
contentPadding: 14,
|
||||
content: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: lengths
|
||||
.map(
|
||||
(value) => Row(
|
||||
children: [
|
||||
Text(value),
|
||||
Radio(
|
||||
value: value, groupValue: length, onChanged: setLength),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList()),
|
||||
);
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
var remember = await bind.sessionGetRemember(id: id) ?? false;
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
cancel() {
|
||||
close();
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
submit() {
|
||||
var text = controller.text.trim();
|
||||
if (text == '') return;
|
||||
gFFI.login(id, text, remember);
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('Password Required')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Remember password'),
|
||||
),
|
||||
value: remember,
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
setState(() => remember = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: cancel,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'OK',
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void wrongPasswordDialog(
|
||||
String id, OverlayDialogManager dialogManager, type, title, text) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
cancel() {
|
||||
close();
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
submit() {
|
||||
enterPasswordDialog(id, dialogManager);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
onPressed: cancel,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'Retry',
|
||||
onPressed: submit,
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
void showServerSettingsWithValue(
|
||||
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
|
||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||
@ -393,232 +270,6 @@ void showServerSettingsWithValue(
|
||||
});
|
||||
}
|
||||
|
||||
void showWaitUacDialog(
|
||||
String id, OverlayDialogManager dialogManager, String type) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show(
|
||||
tag: '$id-wait-uac',
|
||||
(setState, close) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
|
||||
));
|
||||
}
|
||||
|
||||
void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) {
|
||||
RxString groupValue = ''.obs;
|
||||
RxString errUser = ''.obs;
|
||||
RxString errPwd = ''.obs;
|
||||
TextEditingController userController = TextEditingController();
|
||||
TextEditingController pwdController = TextEditingController();
|
||||
|
||||
void onRadioChanged(String? value) {
|
||||
if (value != null) {
|
||||
groupValue.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
const minTextStyle = TextStyle(fontSize: 14);
|
||||
|
||||
var content = Obx(() => Column(children: [
|
||||
Row(
|
||||
children: [
|
||||
Radio(
|
||||
value: '',
|
||||
groupValue: groupValue.value,
|
||||
onChanged: onRadioChanged),
|
||||
Expanded(
|
||||
child:
|
||||
Text(translate('Ask the remote user for authentication'))),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
translate(
|
||||
'Choose this if the remote account is administrator'),
|
||||
style: TextStyle(fontSize: 13))
|
||||
.marginOnly(left: 40),
|
||||
).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Radio(
|
||||
value: 'logon',
|
||||
groupValue: groupValue.value,
|
||||
onChanged: onRadioChanged),
|
||||
Expanded(
|
||||
child: Text(translate(
|
||||
'Transmit the username and password of administrator')),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${translate('Username')}:',
|
||||
style: minTextStyle,
|
||||
).marginOnly(right: 10)),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextField(
|
||||
controller: userController,
|
||||
style: minTextStyle,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 15),
|
||||
hintText: translate('eg: admin'),
|
||||
errorText: errUser.isEmpty ? null : errUser.value),
|
||||
onChanged: (s) {
|
||||
if (s.isNotEmpty) {
|
||||
errUser.value = '';
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
).marginOnly(left: 40),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${translate('Password')}:',
|
||||
style: minTextStyle,
|
||||
).marginOnly(right: 10)),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextField(
|
||||
controller: pwdController,
|
||||
obscureText: true,
|
||||
style: minTextStyle,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 15),
|
||||
errorText: errPwd.isEmpty ? null : errPwd.value),
|
||||
onChanged: (s) {
|
||||
if (s.isNotEmpty) {
|
||||
errPwd.value = '';
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
).marginOnly(left: 40),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(translate('still_click_uac_tip'),
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold))
|
||||
.marginOnly(top: 20)),
|
||||
]));
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show(tag: '$id-request-elevation', (setState, close) {
|
||||
void submit() {
|
||||
if (groupValue.value == 'logon') {
|
||||
if (userController.text.isEmpty) {
|
||||
errUser.value = translate('Empty Username');
|
||||
return;
|
||||
}
|
||||
if (pwdController.text.isEmpty) {
|
||||
errPwd.value = translate('Empty Password');
|
||||
return;
|
||||
}
|
||||
bind.sessionElevateWithLogon(
|
||||
id: id,
|
||||
username: userController.text,
|
||||
password: pwdController.text);
|
||||
} else {
|
||||
bind.sessionElevateDirect(id: id);
|
||||
}
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Request Elevation')),
|
||||
content: content,
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton('OK', onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showOnBlockDialog(
|
||||
String id,
|
||||
String type,
|
||||
String title,
|
||||
String text,
|
||||
OverlayDialogManager dialogManager,
|
||||
) {
|
||||
if (dialogManager.existing('$id-wait-uac') ||
|
||||
dialogManager.existing('$id-request-elevation')) {
|
||||
return;
|
||||
}
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
void submit() {
|
||||
close();
|
||||
showRequestElevationDialog(id, dialogManager);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title,
|
||||
"${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"),
|
||||
actions: [
|
||||
dialogButton('Wait', onPressed: close, isOutline: true),
|
||||
dialogButton('Request Elevation', onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showElevationError(String id, String type, String title, String text,
|
||||
OverlayDialogManager dialogManager) {
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
void submit() {
|
||||
close();
|
||||
showRequestElevationDialog(id, dialogManager);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: () {
|
||||
close();
|
||||
}, isOutline: true),
|
||||
dialogButton('Retry', onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void showWaitAcceptDialog(String id, String type, String title, String text,
|
||||
OverlayDialogManager dialogManager) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
onCancel() {
|
||||
closeConnection();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, title, text),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: onCancel, isOutline: true),
|
||||
],
|
||||
onCancel: onCancel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> validateAsync(String value) async {
|
||||
value = value.trim();
|
||||
if (value.isEmpty) {
|
||||
@ -627,62 +278,3 @@ Future<String?> validateAsync(String value) async {
|
||||
final res = await bind.mainTestIfValidServer(server: value);
|
||||
return res.isEmpty ? null : res;
|
||||
}
|
||||
|
||||
class PasswordWidget extends StatefulWidget {
|
||||
PasswordWidget({Key? key, required this.controller, this.autoFocus = true})
|
||||
: super(key: key);
|
||||
|
||||
final TextEditingController controller;
|
||||
final bool autoFocus;
|
||||
|
||||
@override
|
||||
State<PasswordWidget> createState() => _PasswordWidgetState();
|
||||
}
|
||||
|
||||
class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
bool _passwordVisible = false;
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.autoFocus) {
|
||||
Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.unfocus();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
focusNode: _focusNode,
|
||||
controller: widget.controller,
|
||||
obscureText: !_passwordVisible,
|
||||
//This will obscure text dynamically
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Password'),
|
||||
hintText: translate('Enter your password'),
|
||||
// Here is key idea
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
// Based on passwordVisible state choose the icon
|
||||
_passwordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
color: MyTheme.lightTheme.primaryColor),
|
||||
onPressed: () {
|
||||
// Update the state i.e. toggle the state of passwordVisible variable
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ class AbModel {
|
||||
abError.value = "";
|
||||
final api = "${await bind.mainGetApiServer()}/api/ab/get";
|
||||
try {
|
||||
final resp = await http.post(Uri.parse(api), headers: getHttpHeaders());
|
||||
var authHeaders = getHttpHeaders();
|
||||
authHeaders['Content-Type'] = "application/json";
|
||||
final resp = await http.post(Uri.parse(api), headers: authHeaders);
|
||||
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
|
||||
Map<String, dynamic> json = jsonDecode(resp.body);
|
||||
if (json.containsKey('error')) {
|
||||
|
@ -45,8 +45,12 @@ class InputModel {
|
||||
var command = false;
|
||||
|
||||
// trackpad
|
||||
var trackpadScrollDistance = Offset.zero;
|
||||
final _trackpadSpeed = 0.02;
|
||||
var _trackpadLastDelta = Offset.zero;
|
||||
var _trackpadScrollUnsent = Offset.zero;
|
||||
var _stopFling = true;
|
||||
Timer? _flingTimer;
|
||||
final _flingBaseDelay = 10;
|
||||
|
||||
// mouse
|
||||
final isPhysicalMouse = false.obs;
|
||||
@ -55,10 +59,12 @@ class InputModel {
|
||||
|
||||
get id => parent.target?.id ?? "";
|
||||
|
||||
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
||||
|
||||
InputModel(this.parent);
|
||||
|
||||
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
|
||||
if (!stateGlobal.grabKeyboard) {
|
||||
if (isDesktop && !stateGlobal.grabKeyboard) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@ -111,44 +117,41 @@ class InputModel {
|
||||
}
|
||||
|
||||
void mapKeyboardMode(RawKeyEvent e) {
|
||||
int scanCode;
|
||||
int keyCode;
|
||||
int positionCode = -1;
|
||||
int platformCode = -1;
|
||||
bool down;
|
||||
|
||||
if (e.data is RawKeyEventDataMacOs) {
|
||||
RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs;
|
||||
scanCode = newData.keyCode;
|
||||
keyCode = newData.keyCode;
|
||||
positionCode = newData.keyCode;
|
||||
platformCode = newData.keyCode;
|
||||
} else if (e.data is RawKeyEventDataWindows) {
|
||||
RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows;
|
||||
scanCode = newData.scanCode;
|
||||
keyCode = newData.keyCode;
|
||||
positionCode = newData.scanCode;
|
||||
platformCode = newData.keyCode;
|
||||
} else if (e.data is RawKeyEventDataLinux) {
|
||||
RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux;
|
||||
// scanCode and keyCode of RawKeyEventDataLinux are incorrect.
|
||||
// 1. scanCode means keycode
|
||||
// 2. keyCode means keysym
|
||||
scanCode = 0;
|
||||
keyCode = newData.scanCode;
|
||||
positionCode = newData.scanCode;
|
||||
platformCode = newData.keyCode;
|
||||
} else if (e.data is RawKeyEventDataAndroid) {
|
||||
RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid;
|
||||
scanCode = newData.scanCode + 8;
|
||||
keyCode = newData.keyCode;
|
||||
} else {
|
||||
scanCode = -1;
|
||||
keyCode = -1;
|
||||
}
|
||||
positionCode = newData.scanCode + 8;
|
||||
platformCode = newData.keyCode;
|
||||
} else {}
|
||||
|
||||
if (e is RawKeyDownEvent) {
|
||||
down = true;
|
||||
} else {
|
||||
down = false;
|
||||
}
|
||||
inputRawKey(e.character ?? '', keyCode, scanCode, down);
|
||||
inputRawKey(e.character ?? '', platformCode, positionCode, down);
|
||||
}
|
||||
|
||||
/// Send raw Key Event
|
||||
void inputRawKey(String name, int keyCode, int scanCode, bool down) {
|
||||
void inputRawKey(String name, int platformCode, int positionCode, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
@ -168,8 +171,8 @@ class InputModel {
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
id: id,
|
||||
name: name,
|
||||
keycode: keyCode,
|
||||
scancode: scanCode,
|
||||
platformCode: platformCode,
|
||||
positionCode: positionCode,
|
||||
lockModes: lockModes,
|
||||
downOrUp: down);
|
||||
}
|
||||
@ -199,7 +202,7 @@ class InputModel {
|
||||
/// [down] indicates the key's state(down or up).
|
||||
/// [press] indicates a click event(down and up).
|
||||
void inputKey(String name, {bool? down, bool? press}) {
|
||||
if (!parent.target!.ffiModel.keyboard()) return;
|
||||
if (!keyboardPerm) return;
|
||||
bind.sessionInputKey(
|
||||
id: id,
|
||||
name: name,
|
||||
@ -282,7 +285,7 @@ class InputModel {
|
||||
|
||||
/// Send mouse press event.
|
||||
void sendMouse(String type, MouseButtons button) {
|
||||
if (!parent.target!.ffiModel.keyboard()) return;
|
||||
if (!keyboardPerm) return;
|
||||
bind.sessionSendMouse(
|
||||
id: id,
|
||||
msg: json.encode(modify({'type': type, 'buttons': button.value})));
|
||||
@ -299,7 +302,7 @@ class InputModel {
|
||||
|
||||
/// Send mouse movement event with distance in [x] and [y].
|
||||
void moveMouse(double x, double y) {
|
||||
if (!parent.target!.ffiModel.keyboard()) return;
|
||||
if (!keyboardPerm) return;
|
||||
var x2 = x.toInt();
|
||||
var y2 = y.toInt();
|
||||
bind.sessionSendMouse(
|
||||
@ -307,6 +310,7 @@ class InputModel {
|
||||
}
|
||||
|
||||
void onPointHoverImage(PointerHoverEvent e) {
|
||||
_stopFling = true;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (!isPhysicalMouse.value) {
|
||||
isPhysicalMouse.value = true;
|
||||
@ -324,17 +328,33 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
void onPointerPanZoomStart(PointerPanZoomStartEvent e) {}
|
||||
void onPointerPanZoomStart(PointerPanZoomStartEvent e) {
|
||||
_stopFling = true;
|
||||
}
|
||||
|
||||
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
|
||||
// TODO(support zoom in/out)
|
||||
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
|
||||
var delta = e.panDelta;
|
||||
trackpadScrollDistance += delta;
|
||||
_trackpadLastDelta = delta;
|
||||
_trackpadScrollUnsent += (delta * _trackpadSpeed);
|
||||
var x = _trackpadScrollUnsent.dx.truncate();
|
||||
var y = _trackpadScrollUnsent.dy.truncate();
|
||||
_trackpadScrollUnsent -= Offset(_trackpadScrollUnsent.dx - x.toDouble(),
|
||||
_trackpadScrollUnsent.dy - y.toDouble());
|
||||
|
||||
if (x == 0 && y == 0) {
|
||||
x = delta.dx > 1 ? 1 : (delta.dx < -1 ? -1 : 0);
|
||||
y = delta.dy > 1 ? 1 : (delta.dy < -1 ? -1 : 0);
|
||||
if (x.abs() > y.abs()) {
|
||||
y = 0;
|
||||
} else {
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bind.sessionSendMouse(
|
||||
id: id,
|
||||
msg:
|
||||
'{"type": "trackpad", "x": "${delta.dx.toInt()}", "y": "${delta.dy.toInt()}"}');
|
||||
id: id, msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
|
||||
}
|
||||
|
||||
// Simple simulation for fling.
|
||||
@ -357,18 +377,68 @@ class InputModel {
|
||||
});
|
||||
}
|
||||
|
||||
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
|
||||
var x = _signOrZero(trackpadScrollDistance.dx);
|
||||
var y = _signOrZero(trackpadScrollDistance.dy);
|
||||
var dx = trackpadScrollDistance.dx.abs() ~/ 40;
|
||||
var dy = trackpadScrollDistance.dy.abs() ~/ 40;
|
||||
_scheduleFling(x, y, dx, dy);
|
||||
void _scheduleFling2(double x, double y, int delay) {
|
||||
if ((x == 0 && y == 0) || _stopFling) {
|
||||
return;
|
||||
}
|
||||
|
||||
trackpadScrollDistance = Offset.zero;
|
||||
_flingTimer = Timer(Duration(milliseconds: delay), () {
|
||||
if (_stopFling) {
|
||||
return;
|
||||
}
|
||||
|
||||
final d = 0.95;
|
||||
x *= d;
|
||||
y *= d;
|
||||
final dx0 = x * _trackpadSpeed * 2;
|
||||
final dy0 = y * _trackpadSpeed * 2;
|
||||
|
||||
// Try set delta (x,y) and delay.
|
||||
var dx = dx0.toInt();
|
||||
var dy = dy0.toInt();
|
||||
var delay = _flingBaseDelay;
|
||||
|
||||
// Try set min delta (x,y), and increase delay.
|
||||
if (dx == 0 && dy == 0) {
|
||||
final thr = 25;
|
||||
var vx = thr;
|
||||
var vy = thr;
|
||||
if (dx0 != 0) {
|
||||
vx = 1.0 ~/ dx0.abs();
|
||||
}
|
||||
if (dy0 != 0) {
|
||||
vy = 1.0 ~/ dy0.abs();
|
||||
}
|
||||
if (vx < vy && vx < thr) {
|
||||
delay *= vx;
|
||||
dx = dx0 > 0 ? 1 : (dx0 < 0 ? -1 : 0);
|
||||
} else if (vy < thr) {
|
||||
delay *= vy;
|
||||
dy = dy0 > 0 ? 1 : (dy0 < 0 ? -1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (dx == 0 && dy == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
bind.sessionSendMouse(
|
||||
id: id, msg: '{"type": "trackpad", "x": "$dx", "y": "$dy"}');
|
||||
_scheduleFling2(x, y, delay);
|
||||
});
|
||||
}
|
||||
|
||||
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
|
||||
_stopFling = false;
|
||||
_trackpadScrollUnsent = Offset.zero;
|
||||
_scheduleFling2(
|
||||
_trackpadLastDelta.dx, _trackpadLastDelta.dy, _flingBaseDelay);
|
||||
_trackpadLastDelta = Offset.zero;
|
||||
}
|
||||
|
||||
void onPointDownImage(PointerDownEvent e) {
|
||||
debugPrint("onPointDownImage");
|
||||
_stopFling = true;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
if (isPhysicalMouse.value) {
|
||||
isPhysicalMouse.value = false;
|
||||
|
@ -16,6 +16,9 @@ import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/user_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/plugin/event.dart';
|
||||
import 'package:flutter_hbb/desktop/plugin/desc.dart';
|
||||
import 'package:flutter_hbb/desktop/plugin/widget.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:image/image.dart' as img2;
|
||||
@ -25,7 +28,7 @@ import 'package:get/get.dart';
|
||||
|
||||
import '../common.dart';
|
||||
import '../utils/image.dart' as img;
|
||||
import '../mobile/widgets/dialog.dart';
|
||||
import '../common/widgets/dialog.dart';
|
||||
import 'input_model.dart';
|
||||
import 'platform_model.dart';
|
||||
|
||||
@ -93,7 +96,7 @@ class FfiModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool keyboard() => _permissions['keyboard'] != false;
|
||||
bool get keyboard => _permissions['keyboard'] != false;
|
||||
|
||||
clear() {
|
||||
_pi = PeerInfo();
|
||||
@ -224,6 +227,15 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.chatModel.onVoiceCallIncoming();
|
||||
} else if (name == "update_voice_call_state") {
|
||||
parent.target?.serverModel.updateVoiceCallState(evt);
|
||||
} else if (name == "fingerprint") {
|
||||
FingerprintState.find(peerId).value = evt['fingerprint'] ?? '';
|
||||
} else if (name == "plugin_desc") {
|
||||
updateDesc(evt);
|
||||
} else if (name == "plugin_event") {
|
||||
handlePluginEvent(
|
||||
evt, peerId, (Map<String, dynamic> e) => handleMsgBox(e, peerId));
|
||||
} else if (name == "plugin_reload") {
|
||||
handleReloading(evt, peerId);
|
||||
} else {
|
||||
debugPrint("Unknown event name: $name");
|
||||
}
|
||||
@ -293,6 +305,11 @@ class FfiModel with ChangeNotifier {
|
||||
wrongPasswordDialog(id, dialogManager, type, title, text);
|
||||
} else if (type == 'input-password') {
|
||||
enterPasswordDialog(id, dialogManager);
|
||||
} else if (type == 'session-login' || type == 'session-re-login') {
|
||||
enterUserLoginDialog(id, dialogManager);
|
||||
} else if (type == 'session-login-password' ||
|
||||
type == 'session-login-password') {
|
||||
enterUserLoginAndPasswordDialog(id, dialogManager);
|
||||
} else if (type == 'restarting') {
|
||||
showMsgBox(id, type, title, text, link, false, dialogManager,
|
||||
hasCancel: false);
|
||||
@ -452,6 +469,16 @@ class FfiModel with ChangeNotifier {
|
||||
setViewOnly(peerId,
|
||||
bind.sessionGetToggleOptionSync(id: peerId, arg: 'view-only'));
|
||||
}
|
||||
if (connType == ConnType.defaultConn) {
|
||||
final platform_additions = evt['platform_additions'];
|
||||
if (platform_additions != null && platform_additions != '') {
|
||||
try {
|
||||
_pi.platform_additions = json.decode(platform_additions);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to decode platform_additions $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -525,11 +552,18 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
void setViewOnly(String id, bool value) {
|
||||
if (version_cmp(_pi.version, '1.2.0') < 0) return;
|
||||
if (value) {
|
||||
ShowRemoteCursorState.find(id).value = value;
|
||||
} else {
|
||||
ShowRemoteCursorState.find(id).value =
|
||||
bind.sessionGetToggleOptionSync(id: id, arg: 'show-remote-cursor');
|
||||
// tmp fix for https://github.com/rustdesk/rustdesk/pull/3706#issuecomment-1481242389
|
||||
// because below rx not used in mobile version, so not initialized, below code will cause crash
|
||||
// current our flutter code quality is fucking shit now. !!!!!!!!!!!!!!!!
|
||||
try {
|
||||
if (value) {
|
||||
ShowRemoteCursorState.find(id).value = value;
|
||||
} else {
|
||||
ShowRemoteCursorState.find(id).value =
|
||||
bind.sessionGetToggleOptionSync(id: id, arg: 'show-remote-cursor');
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
if (_viewOnly != value) {
|
||||
_viewOnly = value;
|
||||
@ -554,7 +588,13 @@ class ImageModel with ChangeNotifier {
|
||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||
|
||||
onRgba(Uint8List rgba) {
|
||||
if (_waitForImage[id]!) {
|
||||
final waitforImage = _waitForImage[id];
|
||||
if (waitforImage == null) {
|
||||
debugPrint('Exception, peer $id not found for waiting image');
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitforImage == true) {
|
||||
_waitForImage[id] = false;
|
||||
parent.target?.dialogManager.dismissAll();
|
||||
if (isDesktop) {
|
||||
@ -878,7 +918,7 @@ class CanvasModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||
if (parent.target != null && parent.target!.ffiModel.keyboard()) {
|
||||
if (parent.target != null && parent.target!.ffiModel.keyboard) {
|
||||
// Draw cursor if is not desktop.
|
||||
if (!isDesktop) {
|
||||
parent.target!.cursorModel.moveLocal(x, y);
|
||||
@ -1542,6 +1582,7 @@ class FFI {
|
||||
id = 'pf_$id';
|
||||
} else {
|
||||
chatModel.resetClientMode();
|
||||
connType = ConnType.defaultConn;
|
||||
canvasModel.id = id;
|
||||
imageModel.id = id;
|
||||
cursorModel.id = id;
|
||||
@ -1602,8 +1643,14 @@ class FFI {
|
||||
}
|
||||
|
||||
/// Login with [password], choose if the client should [remember] it.
|
||||
void login(String id, String password, bool remember) {
|
||||
bind.sessionLogin(id: id, password: password, remember: remember);
|
||||
void login(String osUsername, String osPassword, String id, String password,
|
||||
bool remember) {
|
||||
bind.sessionLogin(
|
||||
id: id,
|
||||
osUsername: osUsername,
|
||||
osPassword: osPassword,
|
||||
password: password,
|
||||
remember: remember);
|
||||
}
|
||||
|
||||
/// Close the remote session.
|
||||
@ -1687,6 +1734,10 @@ class PeerInfo {
|
||||
List<Display> displays = [];
|
||||
Features features = Features();
|
||||
List<Resolution> resolutions = [];
|
||||
Map<String, dynamic> platform_additions = {};
|
||||
|
||||
bool get is_wayland => platform_additions['is_wayland'] == true;
|
||||
bool get is_headless => platform_additions['headless'] == true;
|
||||
}
|
||||
|
||||
const canvasKey = 'canvas';
|
||||
|
@ -1236,10 +1236,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: texture_rgba_renderer
|
||||
sha256: "52bc9f217b7b07a760ee837d5a17329ad1f78ae8ed1e3fa612c6f1bed3c77f79"
|
||||
sha256: cb048abdd800468ca40749ca10d1db9d1e6a055d1cde6234c05191293f0c7d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.13"
|
||||
version: "0.0.16"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1571,5 +1571,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
sdks:
|
||||
dart: ">=2.18.0 <4.0.0"
|
||||
dart: ">=2.18.0 <3.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
@ -92,7 +92,7 @@ dependencies:
|
||||
password_strength: ^0.2.0
|
||||
flutter_launcher_icons: ^0.11.0
|
||||
flutter_keyboard_visibility: ^5.4.0
|
||||
texture_rgba_renderer: ^0.0.13
|
||||
texture_rgba_renderer: ^0.0.16
|
||||
percent_indicator: ^4.2.2
|
||||
dropdown_button2: ^2.0.0
|
||||
|
||||
|
@ -59,7 +59,7 @@ def main():
|
||||
b = toks[1].replace('ControlKey(ControlKey::', '').replace("Chr('", '').replace("' as _)),", '').replace(')),', '')
|
||||
KEY_MAP[0] += ' "%s": "%s",\n'%(a, b)
|
||||
print()
|
||||
print('export function checkIfRetry(msgtype: string, title: string, text: string) {')
|
||||
print('export function checkIfRetry(msgtype: string, title: string, text: string, retry_for_relay: boolean) {')
|
||||
print(' return %s'%check_if_retry[0].replace('to_lowercase', 'toLowerCase').replace('contains', 'indexOf').replace('!', '').replace('")', '") < 0'))
|
||||
print(';}')
|
||||
print()
|
||||
|
@ -115,7 +115,7 @@ impl Enigo {
|
||||
|
||||
impl Default for Enigo {
|
||||
fn default() -> Self {
|
||||
let is_x11 = "x11" == hbb_common::platform::linux::get_display_server();
|
||||
let is_x11 = hbb_common::platform::linux::is_x11_or_headless();
|
||||
Self {
|
||||
is_x11,
|
||||
tfc: if is_x11 {
|
||||
|
@ -1,3 +1,4 @@
|
||||
#![allow(dead_code)]
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731
|
||||
//
|
||||
// JP/KR mapping https://github.com/TigerVNC/tigervnc/blob/1a008c1380305648ab50f1d99e73439747e9d61d/vncviewer/win32.c#L267
|
||||
|
@ -156,7 +156,7 @@ impl MouseControllable for Enigo {
|
||||
match button {
|
||||
MouseButton::Back => XBUTTON1 as _,
|
||||
MouseButton::Forward => XBUTTON2 as _,
|
||||
_ => 0,
|
||||
_ => 0,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
@ -186,7 +186,7 @@ impl MouseControllable for Enigo {
|
||||
match button {
|
||||
MouseButton::Back => XBUTTON1 as _,
|
||||
MouseButton::Forward => XBUTTON2 as _,
|
||||
_ => 0,
|
||||
_ => 0,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
@ -215,7 +215,7 @@ impl KeyboardControllable for Enigo {
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
fn key_sequence(&mut self, sequence: &str) {
|
||||
let mut buffer = [0; 2];
|
||||
|
||||
@ -247,15 +247,51 @@ impl KeyboardControllable for Enigo {
|
||||
}
|
||||
|
||||
fn key_down(&mut self, key: Key) -> crate::ResultType {
|
||||
let code = self.key_to_keycode(key);
|
||||
if code == 0 || code == 65535 {
|
||||
return Err("".into());
|
||||
}
|
||||
let res = keybd_event(0, code, 0);
|
||||
if res == 0 {
|
||||
let err = get_error();
|
||||
if !err.is_empty() {
|
||||
return Err(err.into());
|
||||
match &key {
|
||||
Key::Layout(c) => {
|
||||
// to-do: dup code
|
||||
// https://github.com/rustdesk/rustdesk/blob/1bc0dd791ed8344997024dc46626bd2ca7df73d2/src/server/input_service.rs#L1348
|
||||
let code = self.get_layoutdependent_keycode(*c);
|
||||
if code as u16 != 0xFFFF {
|
||||
let vk = code & 0x00FF;
|
||||
let flag = code >> 8;
|
||||
let modifiers = [Key::Shift, Key::Control, Key::Alt];
|
||||
let mod_len = modifiers.len();
|
||||
for pos in 0..mod_len {
|
||||
if flag & (0x0001 << pos) != 0 {
|
||||
self.key_down(modifiers[pos])?;
|
||||
}
|
||||
}
|
||||
|
||||
let res = keybd_event(0, vk, 0);
|
||||
let err = if res == 0 { get_error() } else { "".to_owned() };
|
||||
|
||||
for pos in 0..mod_len {
|
||||
let rpos = mod_len - 1 - pos;
|
||||
if flag & (0x0001 << rpos) != 0 {
|
||||
self.key_up(modifiers[pos]);
|
||||
}
|
||||
}
|
||||
|
||||
if !err.is_empty() {
|
||||
return Err(err.into());
|
||||
}
|
||||
} else {
|
||||
return Err(format!("Failed to get keycode of {}", c).into());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let code = self.key_to_keycode(key);
|
||||
if code == 0 || code == 65535 {
|
||||
return Err("".into());
|
||||
}
|
||||
let res = keybd_event(0, code, 0);
|
||||
if res == 0 {
|
||||
let err = get_error();
|
||||
if !err.is_empty() {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -323,9 +359,6 @@ impl Enigo {
|
||||
}
|
||||
|
||||
fn key_to_keycode(&self, key: Key) -> u16 {
|
||||
unsafe {
|
||||
LAYOUT = std::ptr::null_mut();
|
||||
}
|
||||
// do not use the codes from crate winapi they're
|
||||
// wrongly typed with i32 instead of i16 use the
|
||||
// ones provided by win/keycodes.rs that are prefixed
|
||||
@ -411,30 +444,24 @@ impl Enigo {
|
||||
Key::RightAlt => EVK_RMENU,
|
||||
|
||||
Key::Raw(raw_keycode) => raw_keycode,
|
||||
Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()),
|
||||
Key::Super | Key::Command | Key::Windows | Key::Meta => EVK_LWIN,
|
||||
Key::Layout(..) => {
|
||||
// unreachable
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_layoutdependent_keycode(&self, string: String) -> u16 {
|
||||
// get the first char from the string ignore the rest
|
||||
// ensure its not a multybyte char
|
||||
if let Some(chr) = string.chars().nth(0) {
|
||||
// NOTE VkKeyScanW uses the current keyboard LAYOUT
|
||||
// to specify a LAYOUT use VkKeyScanExW and GetKeyboardLayout
|
||||
// or load one with LoadKeyboardLayoutW
|
||||
let current_window_thread_id =
|
||||
unsafe { GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()) };
|
||||
unsafe { LAYOUT = GetKeyboardLayout(current_window_thread_id) };
|
||||
let keycode_and_shiftstate = unsafe { VkKeyScanExW(chr as _, LAYOUT) };
|
||||
if keycode_and_shiftstate == (EVK_DECIMAL as i16) && chr == '.' {
|
||||
// a workaround of italian keyboard shift + '.' issue
|
||||
EVK_PERIOD as _
|
||||
} else {
|
||||
keycode_and_shiftstate as _
|
||||
}
|
||||
} else {
|
||||
0
|
||||
fn get_layoutdependent_keycode(&self, chr: char) -> u16 {
|
||||
unsafe {
|
||||
LAYOUT = std::ptr::null_mut();
|
||||
}
|
||||
// NOTE VkKeyScanW uses the current keyboard LAYOUT
|
||||
// to specify a LAYOUT use VkKeyScanExW and GetKeyboardLayout
|
||||
// or load one with LoadKeyboardLayoutW
|
||||
let current_window_thread_id =
|
||||
unsafe { GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()) };
|
||||
unsafe { LAYOUT = GetKeyboardLayout(current_window_thread_id) };
|
||||
unsafe { VkKeyScanExW(chr as _, LAYOUT) as _ }
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
|
||||
chrono = "0.4"
|
||||
backtrace = "0.3"
|
||||
libc = "0.2"
|
||||
dlopen = "0.1"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
mac_address = "1.1"
|
||||
|
@ -24,8 +24,8 @@ message VideoFrame {
|
||||
YUV yuv = 8;
|
||||
EncodedVideoFrames h264s = 10;
|
||||
EncodedVideoFrames h265s = 11;
|
||||
EncodedVideoFrames vp8s = 12;
|
||||
}
|
||||
int64 timestamp = 9;
|
||||
}
|
||||
|
||||
message IdPk {
|
||||
@ -53,6 +53,11 @@ message FileTransfer {
|
||||
bool show_hidden = 2;
|
||||
}
|
||||
|
||||
message OSLogin {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message LoginRequest {
|
||||
string username = 1;
|
||||
bytes password = 2;
|
||||
@ -66,6 +71,7 @@ message LoginRequest {
|
||||
bool video_ack_required = 9;
|
||||
uint64 session_id = 10;
|
||||
string version = 11;
|
||||
OSLogin os_login = 12;
|
||||
}
|
||||
|
||||
message ChatMessage { string text = 1; }
|
||||
@ -77,6 +83,7 @@ message Features {
|
||||
message SupportedEncoding {
|
||||
bool h264 = 1;
|
||||
bool h265 = 2;
|
||||
bool vp8 = 3;
|
||||
}
|
||||
|
||||
message PeerInfo {
|
||||
@ -91,6 +98,8 @@ message PeerInfo {
|
||||
Features features = 9;
|
||||
SupportedEncoding encoding = 10;
|
||||
SupportedResolutions resolutions = 11;
|
||||
// Use JSON's key-value format which is friendly for peer to handle.
|
||||
string platform_additions = 12;
|
||||
}
|
||||
|
||||
message LoginResponse {
|
||||
@ -202,11 +211,13 @@ message KeyEvent {
|
||||
bool press = 2;
|
||||
oneof union {
|
||||
ControlKey control_key = 3;
|
||||
// high word, sym key code. win: virtual-key code, linux: keysym ?, macos:
|
||||
// low word, position key code. win: scancode, linux: key code, macos: key code
|
||||
// position key code. win: scancode, linux: key code, macos: key code
|
||||
uint32 chr = 4;
|
||||
uint32 unicode = 5;
|
||||
string seq = 6;
|
||||
// high word. virtual keycode
|
||||
// low word. unicode
|
||||
uint32 win2win_hotkey = 7;
|
||||
}
|
||||
repeated ControlKey modifiers = 8;
|
||||
KeyboardMode mode = 9;
|
||||
@ -456,18 +467,20 @@ enum ImageQuality {
|
||||
Best = 4;
|
||||
}
|
||||
|
||||
message VideoCodecState {
|
||||
message SupportedDecoding {
|
||||
enum PreferCodec {
|
||||
Auto = 0;
|
||||
VPX = 1;
|
||||
VP9 = 1;
|
||||
H264 = 2;
|
||||
H265 = 3;
|
||||
VP8 = 4;
|
||||
}
|
||||
|
||||
int32 score_vpx = 1;
|
||||
int32 score_h264 = 2;
|
||||
int32 score_h265 = 3;
|
||||
int32 ability_vp9 = 1;
|
||||
int32 ability_h264 = 2;
|
||||
int32 ability_h265 = 3;
|
||||
PreferCodec prefer = 4;
|
||||
int32 ability_vp8 = 5;
|
||||
}
|
||||
|
||||
message OptionMessage {
|
||||
@ -485,7 +498,7 @@ message OptionMessage {
|
||||
BoolOption disable_audio = 7;
|
||||
BoolOption disable_clipboard = 8;
|
||||
BoolOption enable_file_transfer = 9;
|
||||
VideoCodecState video_codec_state = 10;
|
||||
SupportedDecoding supported_decoding = 10;
|
||||
int32 custom_fps = 11;
|
||||
BoolOption disable_keyboard = 12;
|
||||
}
|
||||
@ -511,7 +524,6 @@ message AudioFormat {
|
||||
|
||||
message AudioFrame {
|
||||
bytes data = 1;
|
||||
int64 timestamp = 2;
|
||||
}
|
||||
|
||||
// Notify peer to show message box.
|
||||
@ -588,6 +600,17 @@ message SwitchSidesResponse {
|
||||
|
||||
message SwitchBack {}
|
||||
|
||||
message PluginRequest {
|
||||
string id = 1;
|
||||
bytes content = 2;
|
||||
}
|
||||
|
||||
message PluginResponse {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string msg = 3;
|
||||
}
|
||||
|
||||
message Misc {
|
||||
oneof union {
|
||||
ChatMessage chat_message = 4;
|
||||
@ -609,6 +632,8 @@ message Misc {
|
||||
SwitchSidesRequest switch_sides_request = 21;
|
||||
SwitchBack switch_back = 22;
|
||||
Resolution change_resolution = 24;
|
||||
PluginRequest plugin_request = 25;
|
||||
PluginResponse plugin_response = 26;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,11 +64,15 @@ lazy_static::lazy_static! {
|
||||
pub static ref APP_HOME_DIR: Arc<RwLock<String>> = Default::default();
|
||||
}
|
||||
|
||||
// #[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub const LINK_DOCS_HOME: &str = "https://rustdesk.com/docs/en/";
|
||||
pub const LINK_DOCS_X11_REQUIRED: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
|
||||
pub const LINK_HEADLESS_LINUX_SUPPORT: &str =
|
||||
"https://github.com/rustdesk/rustdesk/wiki/Headless-Linux-Support";
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([
|
||||
("rustdesk docs home", "https://rustdesk.com/docs/en/"),
|
||||
("rustdesk docs x11-required", "https://rustdesk.com/docs/en/manual/linux/#x11-required"),
|
||||
("rustdesk docs home", LINK_DOCS_HOME),
|
||||
("rustdesk docs x11-required", LINK_DOCS_X11_REQUIRED),
|
||||
("rustdesk x11 headless", LINK_HEADLESS_LINUX_SUPPORT),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -297,6 +301,7 @@ pub struct TransferSerde {
|
||||
pub read_jobs: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn patch(path: PathBuf) -> PathBuf {
|
||||
if let Some(_tmp) = path.to_str() {
|
||||
#[cfg(windows)]
|
||||
@ -914,15 +919,13 @@ impl PeerConfig {
|
||||
decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
|
||||
config.password = password;
|
||||
store = store || store2;
|
||||
if let Some(v) = config.options.get_mut("rdp_password") {
|
||||
let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
|
||||
*v = password;
|
||||
store = store || store2;
|
||||
}
|
||||
if let Some(v) = config.options.get_mut("os-password") {
|
||||
let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
|
||||
*v = password;
|
||||
store = store || store2;
|
||||
for opt in ["rdp_password", "os-username", "os-password"] {
|
||||
if let Some(v) = config.options.get_mut(opt) {
|
||||
let (encrypted, _, store2) =
|
||||
decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
|
||||
*v = encrypted;
|
||||
store = store || store2;
|
||||
}
|
||||
}
|
||||
if store {
|
||||
config.store(id);
|
||||
@ -940,12 +943,11 @@ impl PeerConfig {
|
||||
let _lock = CONFIG.read().unwrap();
|
||||
let mut config = self.clone();
|
||||
config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
|
||||
if let Some(v) = config.options.get_mut("rdp_password") {
|
||||
*v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)
|
||||
for opt in ["rdp_password", "os-username", "os-password"] {
|
||||
if let Some(v) = config.options.get_mut(opt) {
|
||||
*v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)
|
||||
}
|
||||
}
|
||||
if let Some(v) = config.options.get_mut("os-password") {
|
||||
*v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)
|
||||
};
|
||||
if let Err(err) = store_path(Self::path(id), config) {
|
||||
log::error!("Failed to store config: {}", err);
|
||||
}
|
||||
@ -1359,9 +1361,9 @@ impl UserDefaultConfig {
|
||||
"view_style" => self.get_string(key, "original", vec!["adaptive"]),
|
||||
"scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
||||
"image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]),
|
||||
"codec-preference" => self.get_string(key, "auto", vec!["vp9", "h264", "h265"]),
|
||||
"codec-preference" => self.get_string(key, "auto", vec!["vp8", "vp9", "h264", "h265"]),
|
||||
"custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 100.0),
|
||||
"custom-fps" => self.get_double_string(key, 30.0, 10.0, 120.0),
|
||||
"custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0),
|
||||
_ => self
|
||||
.options
|
||||
.get(key)
|
||||
|
@ -44,6 +44,8 @@ pub use libc;
|
||||
pub mod keyboard;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use sysinfo;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use dlopen;
|
||||
|
||||
#[cfg(feature = "quic")]
|
||||
pub type Stream = quic::Connection;
|
||||
|
@ -5,6 +5,9 @@ lazy_static::lazy_static! {
|
||||
pub static ref DISTRO: Distro = Distro::new();
|
||||
}
|
||||
|
||||
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
|
||||
pub const DISPLAY_SERVER_X11: &str = "x11";
|
||||
|
||||
pub struct Distro {
|
||||
pub name: String,
|
||||
pub version_id: String,
|
||||
@ -12,23 +15,41 @@ pub struct Distro {
|
||||
|
||||
impl Distro {
|
||||
fn new() -> Self {
|
||||
let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned())
|
||||
let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
let version_id =
|
||||
run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release".to_owned())
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
Self { name, version_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_gdm_user(username: &str) -> bool {
|
||||
username == "gdm"
|
||||
// || username == "lightgdm"
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_desktop_wayland() -> bool {
|
||||
get_display_server() == DISPLAY_SERVER_WAYLAND
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_x11_or_headless() -> bool {
|
||||
!is_desktop_wayland()
|
||||
}
|
||||
|
||||
// -1
|
||||
const INVALID_SESSION: &str = "4294967295";
|
||||
|
||||
pub fn get_display_server() -> String {
|
||||
let mut session = get_values_of_seat0([0].to_vec())[0].clone();
|
||||
let mut session = get_values_of_seat0(&[0])[0].clone();
|
||||
if session.is_empty() {
|
||||
// loginctl has not given the expected output. try something else.
|
||||
if let Ok(sid) = std::env::var("XDG_SESSION_ID") {
|
||||
@ -36,14 +57,20 @@ pub fn get_display_server() -> String {
|
||||
session = sid;
|
||||
}
|
||||
if session.is_empty() {
|
||||
session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default();
|
||||
session = run_cmds("cat /proc/self/sessionid").unwrap_or_default();
|
||||
if session == INVALID_SESSION {
|
||||
session = "".to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_display_server_of_session(&session)
|
||||
if session.is_empty() {
|
||||
"".to_owned()
|
||||
} else {
|
||||
get_display_server_of_session(&session)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_display_server_of_session(session: &str) -> String {
|
||||
pub fn get_display_server_of_session(session: &str) -> String {
|
||||
let mut display_server = if let Ok(output) =
|
||||
run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
|
||||
// Check session type of the session
|
||||
@ -61,7 +88,7 @@ fn get_display_server_of_session(session: &str) -> String {
|
||||
.replace("TTY=", "")
|
||||
.trim_end()
|
||||
.into();
|
||||
if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{tty}.\\\\+Xorg\""))
|
||||
if let Ok(xorg_results) = run_cmds(&format!("ps -e | grep \"{tty}.\\\\+Xorg\""))
|
||||
// And check if Xorg is running on that tty
|
||||
{
|
||||
if xorg_results.trim_end() != "" {
|
||||
@ -87,44 +114,68 @@ fn get_display_server_of_session(session: &str) -> String {
|
||||
display_server.to_lowercase()
|
||||
}
|
||||
|
||||
pub fn get_values_of_seat0(indices: Vec<usize>) -> Vec<String> {
|
||||
#[inline]
|
||||
fn line_values(indices: &[usize], line: &str) -> Vec<String> {
|
||||
indices
|
||||
.into_iter()
|
||||
.map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_values_of_seat0(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, true)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, false)
|
||||
}
|
||||
|
||||
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
||||
if let Ok(output) = run_loginctl(None) {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if line.contains("seat0") {
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
return indices
|
||||
.into_iter()
|
||||
.map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
if ignore_gdm_wayland {
|
||||
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
|
||||
&& get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return line_values(indices, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
||||
if let Ok(output) = run_loginctl(None) {
|
||||
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
let d = get_display_server_of_session(sid);
|
||||
if is_active(sid) && d != "tty" {
|
||||
return indices
|
||||
.into_iter()
|
||||
.map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
if is_active(sid) {
|
||||
let d = get_display_server_of_session(sid);
|
||||
if ignore_gdm_wayland {
|
||||
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
|
||||
&& d == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if d == "tty" {
|
||||
continue;
|
||||
}
|
||||
return line_values(indices, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return indices
|
||||
.iter()
|
||||
.map(|_x| "".to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
line_values(indices, "")
|
||||
}
|
||||
|
||||
fn is_active(sid: &str) -> bool {
|
||||
pub fn is_active(sid: &str) -> bool {
|
||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) {
|
||||
String::from_utf8_lossy(&output.stdout).contains("active")
|
||||
} else {
|
||||
@ -132,9 +183,9 @@ fn is_active(sid: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_cmds(cmds: String) -> ResultType<String> {
|
||||
pub fn run_cmds(cmds: &str) -> ResultType<String> {
|
||||
let output = std::process::Command::new("sh")
|
||||
.args(vec!["-c", &cmds])
|
||||
.args(vec!["-c", cmds])
|
||||
.output()?;
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
@ -4,11 +4,15 @@ pub mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use crate::{config::Config, log};
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::process::exit;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
static mut GLOBAL_CALLBACK: Option<Box<dyn Fn()>> = None;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
let mut stack = vec![];
|
||||
backtrace::trace(|frame| {
|
||||
@ -29,6 +33,16 @@ extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
info = "Always use software rendering will be set.".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
if stack.iter().any(|s| {
|
||||
s.to_lowercase().contains("nvidia")
|
||||
|| s.to_lowercase().contains("amf")
|
||||
|| s.to_lowercase().contains("mfx")
|
||||
|| s.contains("cuProfilerStop")
|
||||
}) {
|
||||
Config::set_option("enable-hwcodec".to_string(), "N".to_string());
|
||||
info = "Perhaps hwcodec causing the crash, disable it first".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
log::error!(
|
||||
"Got signal {} and exit. stack:\n{}",
|
||||
sig,
|
||||
@ -51,6 +65,7 @@ extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub fn register_breakdown_handler<T>(callback: T)
|
||||
where
|
||||
T: Fn() + 'static,
|
||||
|
@ -32,7 +32,7 @@ fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result<Socket,
|
||||
if buf_size > 0 {
|
||||
socket.set_recv_buffer_size(buf_size).ok();
|
||||
}
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"Receive buf size of udp {}: {:?}",
|
||||
addr,
|
||||
socket.recv_buffer_size()
|
||||
|
@ -15,7 +15,7 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
|
||||
let mut target = if target_os == "macos" {
|
||||
if target_arch == "x64" {
|
||||
"x64-osx".to_owned()
|
||||
} else if target_arch == "arm64"{
|
||||
} else if target_arch == "arm64" {
|
||||
"arm64-osx".to_owned()
|
||||
} else {
|
||||
format!("{}-{}", target_arch, target_os)
|
||||
|
@ -3,7 +3,8 @@ use hbb_common::env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV};
|
||||
use scrap::{
|
||||
codec::{EncoderApi, EncoderCfg},
|
||||
Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig,
|
||||
VpxVideoCodecId, STRIDE_ALIGN,
|
||||
VpxVideoCodecId::{self, *},
|
||||
STRIDE_ALIGN,
|
||||
};
|
||||
use std::{io::Write, time::Instant};
|
||||
|
||||
@ -49,7 +50,7 @@ fn main() {
|
||||
"benchmark {}x{} bitrate:{}k hw_pixfmt:{:?}",
|
||||
width, height, bitrate_k, args.flag_hw_pixfmt
|
||||
);
|
||||
test_vp9(&yuvs, width, height, bitrate_k, yuv_count);
|
||||
[VP8, VP9].map(|c| test_vpx(c, &yuvs, width, height, bitrate_k, yuv_count));
|
||||
#[cfg(feature = "hwcodec")]
|
||||
{
|
||||
use hwcodec::AVPixelFormat;
|
||||
@ -57,7 +58,7 @@ fn main() {
|
||||
Pixfmt::I420 => AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
Pixfmt::NV12 => AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
};
|
||||
let yuvs = hw::vp9_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt);
|
||||
let yuvs = hw::vpx_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt);
|
||||
hw::test(&yuvs, width, height, bitrate_k, yuv_count, hw_pixfmt);
|
||||
}
|
||||
}
|
||||
@ -87,13 +88,20 @@ fn capture_yuv(yuv_count: usize) -> (Vec<Vec<u8>>, usize, usize) {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_vp9(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, bitrate_k: usize, yuv_count: usize) {
|
||||
fn test_vpx(
|
||||
codec_id: VpxVideoCodecId,
|
||||
yuvs: &Vec<Vec<u8>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
bitrate_k: usize,
|
||||
yuv_count: usize,
|
||||
) {
|
||||
let config = EncoderCfg::VPX(VpxEncoderConfig {
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
timebase: [1, 1000],
|
||||
bitrate: bitrate_k as _,
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
codec: codec_id,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
});
|
||||
let mut encoder = VpxEncoder::new(config).unwrap();
|
||||
@ -104,35 +112,43 @@ fn test_vp9(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, bitrate_k: usize,
|
||||
.unwrap();
|
||||
let _ = encoder.flush().unwrap();
|
||||
}
|
||||
println!("vp9 encode: {:?}", start.elapsed() / yuv_count as _);
|
||||
println!(
|
||||
"{:?} encode: {:?}",
|
||||
codec_id,
|
||||
start.elapsed() / yuv_count as _
|
||||
);
|
||||
|
||||
// prepare data separately
|
||||
let mut vp9s = vec![];
|
||||
let mut vpxs = vec![];
|
||||
let start = Instant::now();
|
||||
for yuv in yuvs {
|
||||
for ref frame in encoder
|
||||
.encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN)
|
||||
.unwrap()
|
||||
{
|
||||
vp9s.push(frame.data.to_vec());
|
||||
vpxs.push(frame.data.to_vec());
|
||||
}
|
||||
for ref frame in encoder.flush().unwrap() {
|
||||
vp9s.push(frame.data.to_vec());
|
||||
vpxs.push(frame.data.to_vec());
|
||||
}
|
||||
}
|
||||
assert_eq!(vp9s.len(), yuv_count);
|
||||
assert_eq!(vpxs.len(), yuv_count);
|
||||
|
||||
let mut decoder = VpxDecoder::new(VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
codec: codec_id,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
})
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
for vp9 in vp9s {
|
||||
let _ = decoder.decode(&vp9);
|
||||
for vpx in vpxs {
|
||||
let _ = decoder.decode(&vpx);
|
||||
let _ = decoder.flush();
|
||||
}
|
||||
println!("vp9 decode: {:?}", start.elapsed() / yuv_count as _);
|
||||
println!(
|
||||
"{:?} decode: {:?}",
|
||||
codec_id,
|
||||
start.elapsed() / yuv_count as _
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
@ -267,7 +283,7 @@ mod hw {
|
||||
Some(info.clone()) == best.h264 || Some(info.clone()) == best.h265
|
||||
}
|
||||
|
||||
pub fn vp9_yuv_to_hw_yuv(
|
||||
pub fn vpx_yuv_to_hw_yuv(
|
||||
yuvs: Vec<Vec<u8>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
|
@ -3,9 +3,9 @@ extern crate scrap;
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use scrap::{i420_to_rgb, Display};
|
||||
#[cfg(windows)]
|
||||
use scrap::{CapturerMag, TraitCapturer};
|
||||
use scrap::{i420_to_rgb, Display};
|
||||
|
||||
fn main() {
|
||||
let n = Display::all().unwrap().len();
|
||||
|
@ -18,7 +18,7 @@ use webm::mux;
|
||||
use webm::mux::Track;
|
||||
|
||||
use scrap::vpxcodec as vpx_encode;
|
||||
use scrap::{TraitCapturer, Capturer, Display, STRIDE_ALIGN};
|
||||
use scrap::{Capturer, Display, TraitCapturer, STRIDE_ALIGN};
|
||||
|
||||
const USAGE: &'static str = "
|
||||
Simple WebM screen capture.
|
||||
|
@ -50,6 +50,7 @@ impl crate::TraitCapturer for Capturer {
|
||||
|
||||
pub enum Frame<'a> {
|
||||
RAW(&'a [u8]),
|
||||
VP8(&'a [u8]),
|
||||
VP9(&'a [u8]),
|
||||
Empty,
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
@ -11,30 +10,31 @@ use crate::hwcodec::*;
|
||||
use crate::mediacodec::{
|
||||
MediaCodecDecoder, MediaCodecDecoders, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT,
|
||||
};
|
||||
use crate::{vpxcodec::*, ImageFormat};
|
||||
use crate::{vpxcodec::*, CodecName, ImageFormat};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::sysinfo::{System, SystemExt};
|
||||
use hbb_common::{
|
||||
anyhow::anyhow,
|
||||
config::PeerConfig,
|
||||
log,
|
||||
message_proto::{video_frame, EncodedVideoFrames, Message, VideoCodecState},
|
||||
message_proto::{
|
||||
supported_decoding::PreferCodec, video_frame, EncodedVideoFrames, Message,
|
||||
SupportedDecoding, SupportedEncoding,
|
||||
},
|
||||
ResultType,
|
||||
};
|
||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
||||
use hbb_common::{
|
||||
config::{Config2, PeerConfig},
|
||||
lazy_static,
|
||||
message_proto::video_codec_state::PreferCodec,
|
||||
};
|
||||
use hbb_common::{config::Config2, lazy_static};
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref PEER_DECODER_STATES: Arc<Mutex<HashMap<i32, VideoCodecState>>> = Default::default();
|
||||
static ref PEER_DECODINGS: Arc<Mutex<HashMap<i32, SupportedDecoding>>> = Default::default();
|
||||
static ref CODEC_NAME: Arc<Mutex<CodecName>> = Arc::new(Mutex::new(CodecName::VP9));
|
||||
}
|
||||
const SCORE_VPX: i32 = 90;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwEncoderConfig {
|
||||
pub codec_name: String,
|
||||
pub name: String,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub bitrate: i32,
|
||||
@ -58,10 +58,6 @@ pub trait EncoderApi {
|
||||
fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()>;
|
||||
}
|
||||
|
||||
pub struct DecoderCfg {
|
||||
pub vpx: VpxDecoderConfig,
|
||||
}
|
||||
|
||||
pub struct Encoder {
|
||||
pub codec: Box<dyn EncoderApi>,
|
||||
}
|
||||
@ -81,7 +77,8 @@ impl DerefMut for Encoder {
|
||||
}
|
||||
|
||||
pub struct Decoder {
|
||||
vpx: VpxDecoder,
|
||||
vp8: VpxDecoder,
|
||||
vp9: VpxDecoder,
|
||||
#[cfg(feature = "hwcodec")]
|
||||
hw: HwDecoders,
|
||||
#[cfg(feature = "hwcodec")]
|
||||
@ -91,10 +88,10 @@ pub struct Decoder {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EncoderUpdate {
|
||||
State(VideoCodecState),
|
||||
pub enum EncodingUpdate {
|
||||
New(SupportedDecoding),
|
||||
Remove,
|
||||
DisableHwIfNotExist,
|
||||
NewOnlyVP9,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
@ -111,7 +108,8 @@ impl Encoder {
|
||||
codec: Box::new(hw),
|
||||
}),
|
||||
Err(e) => {
|
||||
check_config_process(true);
|
||||
check_config_process();
|
||||
*CODEC_NAME.lock().unwrap() = CodecName::VP9;
|
||||
Err(e)
|
||||
}
|
||||
},
|
||||
@ -120,172 +118,158 @@ impl Encoder {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn update_video_encoder(id: i32, update: EncoderUpdate) {
|
||||
pub fn update(id: i32, update: EncodingUpdate) {
|
||||
let mut decodings = PEER_DECODINGS.lock().unwrap();
|
||||
match update {
|
||||
EncodingUpdate::New(decoding) => {
|
||||
decodings.insert(id, decoding);
|
||||
}
|
||||
EncodingUpdate::Remove => {
|
||||
decodings.remove(&id);
|
||||
}
|
||||
EncodingUpdate::NewOnlyVP9 => {
|
||||
decodings.insert(
|
||||
id,
|
||||
SupportedDecoding {
|
||||
ability_vp9: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let vp8_useable = decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_vp8 > 0);
|
||||
#[allow(unused_mut)]
|
||||
let mut h264_name = None;
|
||||
#[allow(unused_mut)]
|
||||
let mut h265_name = None;
|
||||
#[cfg(feature = "hwcodec")]
|
||||
{
|
||||
let mut states = PEER_DECODER_STATES.lock().unwrap();
|
||||
match update {
|
||||
EncoderUpdate::State(state) => {
|
||||
states.insert(id, state);
|
||||
}
|
||||
EncoderUpdate::Remove => {
|
||||
states.remove(&id);
|
||||
}
|
||||
EncoderUpdate::DisableHwIfNotExist => {
|
||||
if !states.contains_key(&id) {
|
||||
states.insert(id, VideoCodecState::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
let name = HwEncoder::current_name();
|
||||
if states.len() > 0 {
|
||||
if enable_hwcodec_option() {
|
||||
let best = HwEncoder::best();
|
||||
let enabled_h264 = best.h264.is_some()
|
||||
&& states.len() > 0
|
||||
&& states.iter().all(|(_, s)| s.score_h264 > 0);
|
||||
let enabled_h265 = best.h265.is_some()
|
||||
&& states.len() > 0
|
||||
&& states.iter().all(|(_, s)| s.score_h265 > 0);
|
||||
|
||||
// Preference first
|
||||
let mut preference = PreferCodec::Auto;
|
||||
let preferences: Vec<_> = states
|
||||
.iter()
|
||||
.filter(|(_, s)| {
|
||||
s.prefer == PreferCodec::VPX.into()
|
||||
|| s.prefer == PreferCodec::H264.into() && enabled_h264
|
||||
|| s.prefer == PreferCodec::H265.into() && enabled_h265
|
||||
})
|
||||
.map(|(_, s)| s.prefer)
|
||||
.collect();
|
||||
if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) {
|
||||
preference = preferences[0].enum_value_or(PreferCodec::Auto);
|
||||
let h264_useable =
|
||||
decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h264 > 0);
|
||||
let h265_useable =
|
||||
decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h265 > 0);
|
||||
if h264_useable {
|
||||
h264_name = best.h264.map_or(None, |c| Some(c.name));
|
||||
}
|
||||
|
||||
match preference {
|
||||
PreferCodec::VPX => *name.lock().unwrap() = None,
|
||||
PreferCodec::H264 => {
|
||||
*name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name))
|
||||
}
|
||||
PreferCodec::H265 => {
|
||||
*name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name))
|
||||
}
|
||||
PreferCodec::Auto => {
|
||||
// score encoder
|
||||
let mut score_vpx = SCORE_VPX;
|
||||
let mut score_h264 = best.h264.as_ref().map_or(0, |c| c.score);
|
||||
let mut score_h265 = best.h265.as_ref().map_or(0, |c| c.score);
|
||||
|
||||
// score decoder
|
||||
score_vpx += states.iter().map(|s| s.1.score_vpx).sum::<i32>();
|
||||
if enabled_h264 {
|
||||
score_h264 += states.iter().map(|s| s.1.score_h264).sum::<i32>();
|
||||
}
|
||||
if enabled_h265 {
|
||||
score_h265 += states.iter().map(|s| s.1.score_h265).sum::<i32>();
|
||||
}
|
||||
|
||||
if enabled_h265 && score_h265 >= score_vpx && score_h265 >= score_h264 {
|
||||
*name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name));
|
||||
} else if enabled_h264
|
||||
&& score_h264 >= score_vpx
|
||||
&& score_h264 >= score_h265
|
||||
{
|
||||
*name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name));
|
||||
} else {
|
||||
*name.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
if h265_useable {
|
||||
h265_name = best.h265.map_or(None, |c| Some(c.name));
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"connection count:{}, used preference:{:?}, encoder:{:?}",
|
||||
states.len(),
|
||||
preference,
|
||||
name.lock().unwrap()
|
||||
)
|
||||
} else {
|
||||
*name.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
{
|
||||
let _ = id;
|
||||
let _ = update;
|
||||
|
||||
let mut name = CODEC_NAME.lock().unwrap();
|
||||
let mut preference = PreferCodec::Auto;
|
||||
let preferences: Vec<_> = decodings
|
||||
.iter()
|
||||
.filter(|(_, s)| {
|
||||
s.prefer == PreferCodec::VP9.into()
|
||||
|| s.prefer == PreferCodec::VP8.into() && vp8_useable
|
||||
|| s.prefer == PreferCodec::H264.into() && h264_name.is_some()
|
||||
|| s.prefer == PreferCodec::H265.into() && h265_name.is_some()
|
||||
})
|
||||
.map(|(_, s)| s.prefer)
|
||||
.collect();
|
||||
if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) {
|
||||
preference = preferences[0].enum_value_or(PreferCodec::Auto);
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn current_hw_encoder_name() -> Option<String> {
|
||||
#[cfg(feature = "hwcodec")]
|
||||
if enable_hwcodec_option() {
|
||||
return HwEncoder::current_name().lock().unwrap().clone();
|
||||
} else {
|
||||
return None;
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut auto_codec = CodecName::VP9;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if vp8_useable && System::new_all().total_memory() <= 4 * 1024 * 1024 * 1024 {
|
||||
// 4 Gb
|
||||
auto_codec = CodecName::VP8
|
||||
}
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
return None;
|
||||
|
||||
match preference {
|
||||
PreferCodec::VP8 => *name = CodecName::VP8,
|
||||
PreferCodec::VP9 => *name = CodecName::VP9,
|
||||
PreferCodec::H264 => *name = h264_name.map_or(auto_codec, |c| CodecName::H264(c)),
|
||||
PreferCodec::H265 => *name = h265_name.map_or(auto_codec, |c| CodecName::H265(c)),
|
||||
PreferCodec::Auto => *name = auto_codec,
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"connection count:{}, used preference:{:?}, encoder:{:?}",
|
||||
decodings.len(),
|
||||
preference,
|
||||
*name
|
||||
)
|
||||
}
|
||||
|
||||
pub fn supported_encoding() -> (bool, bool) {
|
||||
#[inline]
|
||||
pub fn negotiated_codec() -> CodecName {
|
||||
CODEC_NAME.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn supported_encoding() -> SupportedEncoding {
|
||||
#[allow(unused_mut)]
|
||||
let mut encoding = SupportedEncoding {
|
||||
vp8: true,
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
if enable_hwcodec_option() {
|
||||
let best = HwEncoder::best();
|
||||
(
|
||||
best.h264.as_ref().map_or(false, |c| c.score > 0),
|
||||
best.h265.as_ref().map_or(false, |c| c.score > 0),
|
||||
)
|
||||
} else {
|
||||
(false, false)
|
||||
encoding.h264 = best.h264.is_some();
|
||||
encoding.h265 = best.h265.is_some();
|
||||
}
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
(false, false)
|
||||
encoding
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
pub fn video_codec_state(_id: &str) -> VideoCodecState {
|
||||
pub fn supported_decodings(id_for_perfer: Option<&str>) -> SupportedDecoding {
|
||||
#[allow(unused_mut)]
|
||||
let mut decoding = SupportedDecoding {
|
||||
ability_vp8: 1,
|
||||
ability_vp9: 1,
|
||||
prefer: id_for_perfer
|
||||
.map_or(PreferCodec::Auto, |id| Self::codec_preference(id))
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
if enable_hwcodec_option() {
|
||||
let best = HwDecoder::best();
|
||||
return VideoCodecState {
|
||||
score_vpx: SCORE_VPX,
|
||||
score_h264: best.h264.map_or(0, |c| c.score),
|
||||
score_h265: best.h265.map_or(0, |c| c.score),
|
||||
prefer: Self::codec_preference(_id).into(),
|
||||
..Default::default()
|
||||
};
|
||||
decoding.ability_h264 = if best.h264.is_some() { 1 } else { 0 };
|
||||
decoding.ability_h265 = if best.h265.is_some() { 1 } else { 0 };
|
||||
}
|
||||
#[cfg(feature = "mediacodec")]
|
||||
if enable_hwcodec_option() {
|
||||
let score_h264 = if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
92
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let score_h265 = if H265_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
94
|
||||
} else {
|
||||
0
|
||||
};
|
||||
return VideoCodecState {
|
||||
score_vpx: SCORE_VPX,
|
||||
score_h264,
|
||||
score_h265,
|
||||
prefer: Self::codec_preference(_id).into(),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
VideoCodecState {
|
||||
score_vpx: SCORE_VPX,
|
||||
..Default::default()
|
||||
decoding.ability_h264 =
|
||||
if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
decoding.ability_h265 =
|
||||
if H265_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
}
|
||||
decoding
|
||||
}
|
||||
|
||||
pub fn new(config: DecoderCfg) -> Decoder {
|
||||
let vpx = VpxDecoder::new(config.vpx).unwrap();
|
||||
pub fn new() -> Decoder {
|
||||
let vp8 = VpxDecoder::new(VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP8,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
})
|
||||
.unwrap();
|
||||
let vp9 = VpxDecoder::new(VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
})
|
||||
.unwrap();
|
||||
Decoder {
|
||||
vpx,
|
||||
vp8,
|
||||
vp9,
|
||||
#[cfg(feature = "hwcodec")]
|
||||
hw: if enable_hwcodec_option() {
|
||||
HwDecoder::new_decoders()
|
||||
@ -310,8 +294,11 @@ impl Decoder {
|
||||
rgb: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
match frame {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
Decoder::handle_vpxs_video_frame(&mut self.vp8, vp8s, fmt, rgb)
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, fmt, rgb)
|
||||
Decoder::handle_vpxs_video_frame(&mut self.vp9, vp9s, fmt, rgb)
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
@ -349,15 +336,15 @@ impl Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_vp9s_video_frame(
|
||||
fn handle_vpxs_video_frame(
|
||||
decoder: &mut VpxDecoder,
|
||||
vp9s: &EncodedVideoFrames,
|
||||
vpxs: &EncodedVideoFrames,
|
||||
fmt: (ImageFormat, usize),
|
||||
rgb: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
let mut last_frame = Image::new();
|
||||
for vp9 in vp9s.frames.iter() {
|
||||
for frame in decoder.decode(&vp9.data)? {
|
||||
for vpx in vpxs.frames.iter() {
|
||||
for frame in decoder.decode(&vpx.data)? {
|
||||
drop(last_frame);
|
||||
last_frame = frame;
|
||||
}
|
||||
@ -408,14 +395,15 @@ impl Decoder {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
||||
fn codec_preference(id: &str) -> PreferCodec {
|
||||
let codec = PeerConfig::load(id)
|
||||
.options
|
||||
.get("codec-preference")
|
||||
.map_or("".to_owned(), |c| c.to_owned());
|
||||
if codec == "vp9" {
|
||||
PreferCodec::VPX
|
||||
if codec == "vp8" {
|
||||
PreferCodec::VP8
|
||||
} else if codec == "vp9" {
|
||||
PreferCodec::VP9
|
||||
} else if codec == "h264" {
|
||||
PreferCodec::H264
|
||||
} else if codec == "h265" {
|
||||
|
@ -3,10 +3,11 @@ use crate::{
|
||||
hw, ImageFormat, HW_STRIDE_ALIGN,
|
||||
};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
anyhow::{anyhow, Context},
|
||||
bytes::Bytes,
|
||||
config::HwCodecConfig,
|
||||
get_time, lazy_static, log,
|
||||
log,
|
||||
message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame},
|
||||
ResultType,
|
||||
};
|
||||
@ -18,18 +19,13 @@ use hwcodec::{
|
||||
Quality::{self, *},
|
||||
RateControl::{self, *},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HW_ENCODER_NAME: Arc<Mutex<Option<String>>> = Default::default();
|
||||
}
|
||||
|
||||
const CFG_KEY_ENCODER: &str = "bestHwEncoders";
|
||||
const CFG_KEY_DECODER: &str = "bestHwDecoders";
|
||||
|
||||
const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P;
|
||||
pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30];
|
||||
const DEFAULT_GOP: i32 = 60;
|
||||
const DEFAULT_GOP: i32 = i32::MAX;
|
||||
const DEFAULT_HW_QUALITY: Quality = Quality_Default;
|
||||
const DEFAULT_RC: RateControl = RC_DEFAULT;
|
||||
|
||||
@ -48,7 +44,7 @@ impl EncoderApi for HwEncoder {
|
||||
match cfg {
|
||||
EncoderCfg::HW(config) => {
|
||||
let ctx = EncodeContext {
|
||||
name: config.codec_name.clone(),
|
||||
name: config.name.clone(),
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
pixfmt: DEFAULT_PIXFMT,
|
||||
@ -59,12 +55,12 @@ impl EncoderApi for HwEncoder {
|
||||
quality: DEFAULT_HW_QUALITY,
|
||||
rc: DEFAULT_RC,
|
||||
};
|
||||
let format = match Encoder::format_from_name(config.codec_name.clone()) {
|
||||
let format = match Encoder::format_from_name(config.name.clone()) {
|
||||
Ok(format) => format,
|
||||
Err(_) => {
|
||||
return Err(anyhow!(format!(
|
||||
"failed to get format from name:{}",
|
||||
config.codec_name
|
||||
config.name
|
||||
)))
|
||||
}
|
||||
};
|
||||
@ -107,7 +103,6 @@ impl EncoderApi for HwEncoder {
|
||||
DataFormat::H264 => vf.set_h264s(frames),
|
||||
DataFormat::H265 => vf.set_h265s(frames),
|
||||
}
|
||||
vf.timestamp = get_time();
|
||||
msg_out.set_video_frame(vf);
|
||||
Ok(msg_out)
|
||||
} else {
|
||||
@ -133,10 +128,6 @@ impl HwEncoder {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn current_name() -> Arc<Mutex<Option<String>>> {
|
||||
HW_ENCODER_NAME.clone()
|
||||
}
|
||||
|
||||
pub fn encode(&mut self, bgra: &[u8]) -> ResultType<Vec<EncodeFrame>> {
|
||||
match self.pixfmt {
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P => hw::hw_bgra_to_i420(
|
||||
@ -208,7 +199,7 @@ impl HwDecoder {
|
||||
}
|
||||
}
|
||||
if fail {
|
||||
check_config_process(true);
|
||||
check_config_process();
|
||||
}
|
||||
HwDecoders { h264, h265 }
|
||||
}
|
||||
@ -332,13 +323,11 @@ pub fn check_config() {
|
||||
log::error!("Failed to serialize codec info");
|
||||
}
|
||||
|
||||
pub fn check_config_process(force_reset: bool) {
|
||||
pub fn check_config_process() {
|
||||
use hbb_common::sysinfo::{ProcessExt, System, SystemExt};
|
||||
|
||||
std::thread::spawn(move || {
|
||||
if force_reset {
|
||||
HwCodecConfig::remove();
|
||||
}
|
||||
HwCodecConfig::remove();
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(file_name) = exe.file_name().to_owned() {
|
||||
let s = System::new_all();
|
||||
@ -353,7 +342,21 @@ pub fn check_config_process(force_reset: bool) {
|
||||
let second = 3;
|
||||
std::thread::sleep(std::time::Duration::from_secs(second));
|
||||
// kill: Different platforms have different results
|
||||
child.kill().ok();
|
||||
allow_err!(child.kill());
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => log::info!("Check hwcodec config, exit with: {status}"),
|
||||
Ok(None) => {
|
||||
log::info!(
|
||||
"Check hwcodec config, status not ready yet, let's really wait"
|
||||
);
|
||||
let res = child.wait();
|
||||
log::info!("Check hwcodec config, wait result: {res:?}");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Check hwcodec config, error attempting to wait: {e}")
|
||||
}
|
||||
}
|
||||
HwCodecConfig::refresh();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub use self::vpxcodec::*;
|
||||
use hbb_common::message_proto::{video_frame, VideoFrame};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(quartz)] {
|
||||
@ -63,6 +64,9 @@ pub fn would_block_if_equal(old: &mut Vec<u8>, b: &[u8]) -> std::io::Result<()>
|
||||
|
||||
pub trait TraitCapturer {
|
||||
fn set_use_yuv(&mut self, use_yuv: bool);
|
||||
|
||||
// We doesn't support
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result<Frame<'a>>;
|
||||
|
||||
#[cfg(windows)]
|
||||
@ -74,7 +78,7 @@ pub trait TraitCapturer {
|
||||
#[cfg(x11)]
|
||||
#[inline]
|
||||
pub fn is_x11() -> bool {
|
||||
"x11" == hbb_common::platform::linux::get_display_server()
|
||||
hbb_common::platform::linux::is_x11_or_headless()
|
||||
}
|
||||
|
||||
#[cfg(x11)]
|
||||
@ -92,3 +96,55 @@ pub fn is_cursor_embedded() -> bool {
|
||||
pub fn is_cursor_embedded() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CodecName {
|
||||
VP8,
|
||||
VP9,
|
||||
H264(String),
|
||||
H265(String),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum CodecFormat {
|
||||
VP8,
|
||||
VP9,
|
||||
H264,
|
||||
H265,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&VideoFrame> for CodecFormat {
|
||||
fn from(it: &VideoFrame) -> Self {
|
||||
match it.union {
|
||||
Some(video_frame::Union::Vp8s(_)) => CodecFormat::VP8,
|
||||
Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9,
|
||||
Some(video_frame::Union::H264s(_)) => CodecFormat::H264,
|
||||
Some(video_frame::Union::H265s(_)) => CodecFormat::H265,
|
||||
_ => CodecFormat::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CodecName> for CodecFormat {
|
||||
fn from(value: &CodecName) -> Self {
|
||||
match value {
|
||||
CodecName::VP8 => Self::VP8,
|
||||
CodecName::VP9 => Self::VP9,
|
||||
CodecName::H264(_) => Self::H264,
|
||||
CodecName::H265(_) => Self::H265,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for CodecFormat {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
CodecFormat::VP8 => "VP8".into(),
|
||||
CodecFormat::VP9 => "VP9".into(),
|
||||
CodecFormat::H264 => "H264".into(),
|
||||
CodecFormat::H265 => "H265".into(),
|
||||
CodecFormat::Unknown => "Unknow".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::CodecFormat;
|
||||
#[cfg(feature = "hwcodec")]
|
||||
use hbb_common::anyhow::anyhow;
|
||||
use hbb_common::{
|
||||
@ -21,13 +22,6 @@ use webm::mux::{self, Segment, Track, VideoTrack, Writer};
|
||||
|
||||
const MIN_SECS: u64 = 1;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RecordCodecID {
|
||||
VP9,
|
||||
H264,
|
||||
H265,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RecorderContext {
|
||||
pub server: bool,
|
||||
@ -36,7 +30,7 @@ pub struct RecorderContext {
|
||||
pub filename: String,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub codec_id: RecordCodecID,
|
||||
pub format: CodecFormat,
|
||||
pub tx: Option<Sender<RecordState>>,
|
||||
}
|
||||
|
||||
@ -55,8 +49,9 @@ impl RecorderContext {
|
||||
}
|
||||
let file = if self.server { "s" } else { "c" }.to_string()
|
||||
+ &self.id.clone()
|
||||
+ &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string()
|
||||
+ if self.codec_id == RecordCodecID::VP9 {
|
||||
+ &chrono::Local::now().format("_%Y%m%d%H%M%S_").to_string()
|
||||
+ &self.format.to_string()
|
||||
+ if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 {
|
||||
".webm"
|
||||
} else {
|
||||
".mp4"
|
||||
@ -107,8 +102,8 @@ impl DerefMut for Recorder {
|
||||
impl Recorder {
|
||||
pub fn new(mut ctx: RecorderContext) -> ResultType<Self> {
|
||||
ctx.set_filename()?;
|
||||
let recorder = match ctx.codec_id {
|
||||
RecordCodecID::VP9 => Recorder {
|
||||
let recorder = match ctx.format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 => Recorder {
|
||||
inner: Box::new(WebmRecorder::new(ctx.clone())?),
|
||||
ctx,
|
||||
},
|
||||
@ -126,8 +121,8 @@ impl Recorder {
|
||||
|
||||
fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> {
|
||||
ctx.set_filename()?;
|
||||
self.inner = match ctx.codec_id {
|
||||
RecordCodecID::VP9 => Box::new(WebmRecorder::new(ctx.clone())?),
|
||||
self.inner = match ctx.format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 => Box::new(WebmRecorder::new(ctx.clone())?),
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Box::new(HwRecorder::new(ctx.clone())?),
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
@ -148,10 +143,19 @@ impl Recorder {
|
||||
|
||||
pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> {
|
||||
match frame {
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
if self.ctx.codec_id != RecordCodecID::VP9 {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
if self.ctx.format != CodecFormat::VP8 {
|
||||
self.change(RecorderContext {
|
||||
codec_id: RecordCodecID::VP9,
|
||||
format: CodecFormat::VP8,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
vp8s.frames.iter().map(|f| self.write_video(f)).count();
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
if self.ctx.format != CodecFormat::VP9 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::VP9,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
@ -159,25 +163,25 @@ impl Recorder {
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
if self.ctx.codec_id != RecordCodecID::H264 {
|
||||
if self.ctx.format != CodecFormat::H264 {
|
||||
self.change(RecorderContext {
|
||||
codec_id: RecordCodecID::H264,
|
||||
format: CodecFormat::H264,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
if self.ctx.codec_id == RecordCodecID::H264 {
|
||||
if self.ctx.format == CodecFormat::H264 {
|
||||
h264s.frames.iter().map(|f| self.write_video(f)).count();
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H265s(h265s) => {
|
||||
if self.ctx.codec_id != RecordCodecID::H265 {
|
||||
if self.ctx.format != CodecFormat::H265 {
|
||||
self.change(RecorderContext {
|
||||
codec_id: RecordCodecID::H265,
|
||||
format: CodecFormat::H265,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
if self.ctx.codec_id == RecordCodecID::H265 {
|
||||
if self.ctx.format == CodecFormat::H265 {
|
||||
h265s.frames.iter().map(|f| self.write_video(f)).count();
|
||||
}
|
||||
}
|
||||
@ -221,7 +225,11 @@ impl RecorderApi for WebmRecorder {
|
||||
ctx.width as _,
|
||||
ctx.height as _,
|
||||
None,
|
||||
mux::VideoCodecId::VP9,
|
||||
if ctx.format == CodecFormat::VP9 {
|
||||
mux::VideoCodecId::VP9
|
||||
} else {
|
||||
mux::VideoCodecId::VP8
|
||||
},
|
||||
);
|
||||
Ok(WebmRecorder {
|
||||
vt,
|
||||
@ -279,7 +287,7 @@ impl RecorderApi for HwRecorder {
|
||||
filename: ctx.filename.clone(),
|
||||
width: ctx.width,
|
||||
height: ctx.height,
|
||||
is265: ctx.codec_id == RecordCodecID::H265,
|
||||
is265: ctx.format == CodecFormat::H265,
|
||||
framerate: crate::hwcodec::DEFAULT_TIME_BASE[1] as _,
|
||||
})
|
||||
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
use hbb_common::anyhow::{anyhow, Context};
|
||||
use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame};
|
||||
use hbb_common::{get_time, ResultType};
|
||||
use hbb_common::ResultType;
|
||||
|
||||
use crate::STRIDE_ALIGN;
|
||||
use crate::{codec::EncoderApi, ImageFormat};
|
||||
@ -30,6 +30,7 @@ pub struct VpxEncoder {
|
||||
ctx: vpx_codec_ctx_t,
|
||||
width: usize,
|
||||
height: usize,
|
||||
id: VpxVideoCodecId,
|
||||
}
|
||||
|
||||
pub struct VpxDecoder {
|
||||
@ -97,15 +98,10 @@ impl EncoderApi for VpxEncoder {
|
||||
{
|
||||
match cfg {
|
||||
crate::codec::EncoderCfg::VPX(config) => {
|
||||
let i;
|
||||
if cfg!(feature = "VP8") {
|
||||
i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
|
||||
};
|
||||
} else {
|
||||
i = call_vpx_ptr!(vpx_codec_vp9_cx());
|
||||
}
|
||||
let i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
|
||||
};
|
||||
let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
|
||||
call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
|
||||
|
||||
@ -187,12 +183,17 @@ impl EncoderApi for VpxEncoder {
|
||||
VP9E_SET_TILE_COLUMNS as _,
|
||||
4 as c_int
|
||||
));
|
||||
} else if config.codec == VpxVideoCodecId::VP8 {
|
||||
// https://github.com/webmproject/libvpx/blob/972149cafeb71d6f08df89e91a0130d6a38c4b15/vpx/vp8cx.h#L172
|
||||
// https://groups.google.com/a/webmproject.org/g/webm-discuss/c/DJhSrmfQ61M
|
||||
call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 12,));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ctx,
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
id: config.codec,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("encoder type mismatch")),
|
||||
@ -213,7 +214,7 @@ impl EncoderApi for VpxEncoder {
|
||||
|
||||
// to-do: flush periodically, e.g. 1 second
|
||||
if frames.len() > 0 {
|
||||
Ok(VpxEncoder::create_msg(frames))
|
||||
Ok(VpxEncoder::create_msg(self.id, frames))
|
||||
} else {
|
||||
Err(anyhow!("no valid frame"))
|
||||
}
|
||||
@ -280,14 +281,17 @@ impl VpxEncoder {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_msg(vp9s: Vec<EncodedVideoFrame>) -> Message {
|
||||
pub fn create_msg(codec_id: VpxVideoCodecId, frames: Vec<EncodedVideoFrame>) -> Message {
|
||||
let mut msg_out = Message::new();
|
||||
let mut vf = VideoFrame::new();
|
||||
vf.set_vp9s(EncodedVideoFrames {
|
||||
frames: vp9s.into(),
|
||||
let vpxs = EncodedVideoFrames {
|
||||
frames: frames.into(),
|
||||
..Default::default()
|
||||
});
|
||||
vf.timestamp = get_time();
|
||||
};
|
||||
match codec_id {
|
||||
VpxVideoCodecId::VP8 => vf.set_vp8s(vpxs),
|
||||
VpxVideoCodecId::VP9 => vf.set_vp9s(vpxs),
|
||||
}
|
||||
msg_out.set_video_frame(vf);
|
||||
msg_out
|
||||
}
|
||||
@ -383,15 +387,10 @@ impl VpxDecoder {
|
||||
pub fn new(config: VpxDecoderConfig) -> Result<Self> {
|
||||
// This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can
|
||||
// cause UB if uninitialized.
|
||||
let i;
|
||||
if cfg!(feature = "VP8") {
|
||||
i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
|
||||
};
|
||||
} else {
|
||||
i = call_vpx_ptr!(vpx_codec_vp9_dx());
|
||||
}
|
||||
let i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
|
||||
};
|
||||
let mut ctx = Default::default();
|
||||
let cfg = vpx_codec_dec_cfg_t {
|
||||
threads: if config.num_threads == 0 {
|
||||
|
@ -1,3 +1,3 @@
|
||||
pub mod capturable;
|
||||
pub mod pipewire;
|
||||
mod pipewire_dbus;
|
||||
pub mod capturable;
|
||||
|
@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "simple_rc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
walkdir = "2"
|
||||
confy = { git = "https://github.com/open-trade/confy" }
|
||||
hbb_common = { path = "../hbb_common" }
|
@ -1,23 +0,0 @@
|
||||
extern crate simple_rc;
|
||||
|
||||
use simple_rc::*;
|
||||
|
||||
fn main() {
|
||||
{
|
||||
const CONF_FILE: &str = "simple_rc.toml";
|
||||
generate(CONF_FILE).unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
generate_with_conf(&Config {
|
||||
outfile: "src/rc.rs".to_owned(),
|
||||
confs: vec![ConfigItem {
|
||||
inc: "D:/projects/windows/RustDeskTempTopMostWindow/x64/Release/xxx".to_owned(),
|
||||
// exc: vec!["*.dll".to_owned(), "*.exe".to_owned()],
|
||||
exc: vec![],
|
||||
suppressed_front: "D:/projects/windows".to_owned(),
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
# The output source file
|
||||
outfile = "src/rc.rs"
|
||||
|
||||
# The resource config list.
|
||||
[[confs]]
|
||||
# The file or director to integrate.
|
||||
inc = "D:/projects/windows/RustDeskTempTopMostWindow/x64/Release/xxx"
|
||||
# The exclusions.
|
||||
exc = ["*.dll", "*.exe"]
|
||||
# The front path that will ignore for extracting.
|
||||
# The following config will make base output path to be "RustDeskTempTopMostWindow/x64/Release/xxx".
|
||||
suppressed_front = "D:/projects/windows"
|
@ -1,208 +0,0 @@
|
||||
use hbb_common::{bail, ResultType};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fs::File, io::prelude::*, path::Path};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
//mod rc;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)]
|
||||
pub struct ConfigItem {
|
||||
// include directory or file
|
||||
pub inc: String,
|
||||
// exclude files
|
||||
pub exc: Vec<String>,
|
||||
// out_path = origin_path - suppressed_front
|
||||
pub suppressed_front: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
// output source file
|
||||
pub outfile: String,
|
||||
// config items
|
||||
pub confs: Vec<ConfigItem>,
|
||||
}
|
||||
|
||||
pub fn get_outin_files<'a>(item: &'a ConfigItem) -> ResultType<HashMap<String, String>> {
|
||||
let mut outin_filemap = HashMap::new();
|
||||
|
||||
for entry in WalkDir::new(&item.inc).follow_links(true) {
|
||||
let path = entry?.into_path();
|
||||
if path.is_file() {
|
||||
let mut exclude = false;
|
||||
for excfile in item.exc.iter() {
|
||||
if excfile.starts_with("*.") {
|
||||
if let Some(ext) = path.extension().and_then(|x| x.to_str()) {
|
||||
if excfile.ends_with(&format!(".{}", ext)) {
|
||||
exclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if path.ends_with(Path::new(excfile)) {
|
||||
exclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if exclude {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut suppressed_front = item.suppressed_front.clone();
|
||||
if !suppressed_front.is_empty() && suppressed_front.ends_with('/') {
|
||||
suppressed_front.push('/');
|
||||
}
|
||||
let outpath = path.strip_prefix(Path::new(&suppressed_front))?;
|
||||
let outfile = if outpath.is_absolute() {
|
||||
match outpath
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.map(|f| f.to_string())
|
||||
{
|
||||
None => {
|
||||
bail!("Failed to get filename of {}", outpath.display());
|
||||
}
|
||||
Some(s) => s,
|
||||
}
|
||||
} else {
|
||||
match outpath.to_str() {
|
||||
None => {
|
||||
bail!("Failed to convert {} to string", outpath.display());
|
||||
}
|
||||
// Simple replace \ to / here.
|
||||
// A better way is to use lib [path-slash](https://github.com/rhysd/path-slash)
|
||||
Some(s) => s.to_string().replace("\\", "/"),
|
||||
}
|
||||
};
|
||||
let infile = match path.canonicalize()?.to_str() {
|
||||
None => {
|
||||
bail!("Failed to get file path of {}", path.display());
|
||||
}
|
||||
Some(s) => s.to_string(),
|
||||
};
|
||||
if let Some(_) = outin_filemap.insert(outfile.clone(), infile) {
|
||||
bail!("outfile {} is set before", outfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(outin_filemap)
|
||||
}
|
||||
|
||||
pub fn generate(conf_file: &str) -> ResultType<()> {
|
||||
let conf = confy::load_path(conf_file)?;
|
||||
generate_with_conf(&conf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_with_conf<'a>(conf: &'a Config) -> ResultType<()> {
|
||||
let mut outfile = File::create(&conf.outfile)?;
|
||||
|
||||
outfile.write(
|
||||
br##"use hbb_common::{bail, ResultType};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::prelude::*,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
"##,
|
||||
)?;
|
||||
|
||||
outfile.write(b"#[allow(dead_code)]\n")?;
|
||||
outfile.write(b"pub fn extract_resources(root_path: &str) -> ResultType<()> {\n")?;
|
||||
outfile.write(b" let mut resources: Vec<(&str, &[u8])> = Vec::new();\n")?;
|
||||
|
||||
let mut outin_files = HashMap::new();
|
||||
for item in conf.confs.iter() {
|
||||
for (o, i) in get_outin_files(item)?.into_iter() {
|
||||
if let Some(_) = outin_files.insert(o.clone(), i) {
|
||||
bail!("outfile {} is set before", o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut count = 1;
|
||||
for (o, i) in outin_files.iter() {
|
||||
let mut infile = File::open(&i)?;
|
||||
let mut buffer = Vec::<u8>::new();
|
||||
infile.read_to_end(&mut buffer)?;
|
||||
|
||||
let var_outfile = format!("outfile_{}", count);
|
||||
let var_outdata = format!("outdata_{}", count);
|
||||
|
||||
write!(outfile, " let {} = \"{}\";\n", var_outfile, o)?;
|
||||
write!(outfile, " let {}: &[u8] = &[\n ", var_outdata)?;
|
||||
|
||||
let mut line_num = 20;
|
||||
for v in buffer {
|
||||
if line_num == 0 {
|
||||
write!(outfile, "\n ")?;
|
||||
line_num = 20;
|
||||
}
|
||||
write!(outfile, "{:#04x}, ", v)?;
|
||||
line_num -= 1;
|
||||
}
|
||||
write!(outfile, "\n ];\n")?;
|
||||
|
||||
write!(
|
||||
outfile,
|
||||
" resources.push(({}, &{}));\n",
|
||||
var_outfile, var_outdata
|
||||
)?;
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
outfile.write(b" do_extract(root_path, resources)?;\n")?;
|
||||
outfile.write(b" Ok(())\n")?;
|
||||
outfile.write(b"}\n")?;
|
||||
|
||||
outfile.write(
|
||||
br##"
|
||||
#[allow(dead_code)]
|
||||
fn do_extract(root_path: &str, resources: Vec<(&str, &[u8])>) -> ResultType<()> {
|
||||
let mut root_path = root_path.replace("\\", "/");
|
||||
if !root_path.ends_with('/') {
|
||||
root_path.push('/');
|
||||
}
|
||||
let root_path = Path::new(&root_path);
|
||||
for (outfile, data) in resources {
|
||||
let outfile_path = root_path.join(outfile);
|
||||
match outfile_path.parent().and_then(|p| p.to_str()) {
|
||||
None => {
|
||||
bail!("Failed to get parent of {}", outfile_path.display());
|
||||
}
|
||||
Some(p) => {
|
||||
fs::create_dir_all(p)?;
|
||||
let mut of = File::create(outfile_path)?;
|
||||
of.write_all(data)?;
|
||||
of.flush()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
"##,
|
||||
)?;
|
||||
|
||||
outfile.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = 2 + 2;
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_extract() {
|
||||
// use super::*;
|
||||
// rc::extract_resources("D:").unwrap();
|
||||
// }
|
||||
}
|
@ -7,5 +7,4 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4"
|
||||
libloading = "0.7"
|
||||
hbb_common = { path = "../hbb_common" }
|
||||
hbb_common = { path = "../hbb_common" }
|
@ -2,18 +2,21 @@
|
||||
pub mod win10;
|
||||
|
||||
use hbb_common::{bail, lazy_static, ResultType};
|
||||
use std::{path::Path, sync::Mutex};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg(windows)]
|
||||
lazy_static::lazy_static! {
|
||||
// If device is uninstalled though "Device Manager" Window.
|
||||
// Rustdesk is unable to handle device any more...
|
||||
static ref H_SW_DEVICE: Mutex<u64> = Mutex::new(0);
|
||||
static ref MONITOR_PLUGIN: Mutex<Vec<u32>> = Mutex::new(Vec::new());
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[cfg(windows)]
|
||||
pub fn get_dirver_install_path() -> &'static str {
|
||||
pub fn get_driver_install_path() -> &'static str {
|
||||
win10::DRIVER_INSTALL_PATH
|
||||
}
|
||||
|
||||
@ -137,68 +140,48 @@ pub fn close_device() {
|
||||
unsafe {
|
||||
win10::idd::DeviceClose(*H_SW_DEVICE.lock().unwrap() as win10::idd::HSWDEVICE);
|
||||
*H_SW_DEVICE.lock().unwrap() = 0;
|
||||
MONITOR_PLUGIN.lock().unwrap().clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn plug_in_monitor() -> ResultType<()> {
|
||||
pub fn plug_in_monitor(_monitor_index: u32, _edid: u32, _retries: u32) -> ResultType<()> {
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
let monitor_index = 0 as u32;
|
||||
let mut plug_in_monitors = MONITOR_PLUGIN.lock().unwrap();
|
||||
for i in 0..plug_in_monitors.len() {
|
||||
if let Some(d) = plug_in_monitors.get(i) {
|
||||
if *d == monitor_index {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
}
|
||||
if win10::idd::MonitorPlugIn(monitor_index, 0, 30) == win10::idd::FALSE {
|
||||
bail!("{}", win10::get_last_msg()?);
|
||||
}
|
||||
(*plug_in_monitors).push(monitor_index);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn plug_out_monitor() -> ResultType<()> {
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
let monitor_index = 0 as u32;
|
||||
if win10::idd::MonitorPlugOut(monitor_index) == win10::idd::FALSE {
|
||||
bail!("{}", win10::get_last_msg()?);
|
||||
}
|
||||
let mut plug_in_monitors = MONITOR_PLUGIN.lock().unwrap();
|
||||
for i in 0..plug_in_monitors.len() {
|
||||
if let Some(d) = plug_in_monitors.get(i) {
|
||||
if *d == monitor_index {
|
||||
plug_in_monitors.remove(i);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn update_monitor_modes() -> ResultType<()> {
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
let monitor_index = 0 as u32;
|
||||
let mut modes = vec![win10::idd::MonitorMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
sync: 60,
|
||||
}];
|
||||
if win10::idd::FALSE
|
||||
== win10::idd::MonitorModesUpdate(
|
||||
monitor_index as win10::idd::UINT,
|
||||
modes.len() as win10::idd::UINT,
|
||||
modes.as_mut_ptr(),
|
||||
)
|
||||
if win10::idd::MonitorPlugIn(_monitor_index as _, _edid as _, _retries as _)
|
||||
== win10::idd::FALSE
|
||||
{
|
||||
bail!("{}", win10::get_last_msg()?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn plug_out_monitor(_monitor_index: u32) -> ResultType<()> {
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
if win10::idd::MonitorPlugOut(_monitor_index) == win10::idd::FALSE {
|
||||
bail!("{}", win10::get_last_msg()?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
type PMonitorMode = win10::idd::PMonitorMode;
|
||||
#[cfg(not(windows))]
|
||||
type PMonitorMode = *mut std::ffi::c_void;
|
||||
|
||||
#[no_mangle]
|
||||
pub fn update_monitor_modes(
|
||||
_monitor_index: u32,
|
||||
_mode_count: u32,
|
||||
_modes: PMonitorMode,
|
||||
) -> ResultType<()> {
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
if win10::idd::FALSE
|
||||
== win10::idd::MonitorModesUpdate(_monitor_index as _, _mode_count as _, _modes)
|
||||
{
|
||||
bail!("{}", win10::get_last_msg()?);
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice)
|
||||
}
|
||||
if (created == TRUE)
|
||||
{
|
||||
SetLastMsg("Device is created before, please uninstall it first\n");
|
||||
SetLastMsg("Device is already created, please destroy it first\n");
|
||||
if (g_printMsg)
|
||||
{
|
||||
printf(g_lastMsg);
|
||||
@ -288,7 +288,7 @@ BOOL MonitorPlugIn(UINT index, UINT edid, INT retries)
|
||||
|
||||
if (retries < 0)
|
||||
{
|
||||
SetLastMsg("invalid tries %d\n", retries);
|
||||
SetLastMsg("Invalid tries %d\n", retries);
|
||||
if (g_printMsg)
|
||||
{
|
||||
printf(g_lastMsg);
|
||||
|
@ -3,7 +3,7 @@ use virtual_display;
|
||||
|
||||
fn prompt_input() -> u8 {
|
||||
println!("Press key execute:");
|
||||
println!(" 1. 'x' 1. exit");
|
||||
println!(" 1. 'q' 1. quit");
|
||||
println!(" 2. 'i' 2. install or update driver");
|
||||
println!(" 3. 'u' 3. uninstall driver");
|
||||
println!(" 4. 'c' 4. create device");
|
||||
@ -18,18 +18,18 @@ fn prompt_input() -> u8 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn plug_in() {
|
||||
fn plug_in(monitor_index: u32) {
|
||||
println!("Plug in monitor begin");
|
||||
if let Err(e) = virtual_display::plug_in_monitor() {
|
||||
if let Err(e) = virtual_display::plug_in_monitor(monitor_index as _) {
|
||||
println!("{}", e);
|
||||
} else {
|
||||
println!("Plug in monitor done");
|
||||
}
|
||||
}
|
||||
|
||||
fn plug_out() {
|
||||
fn plug_out(monitor_index: u32) {
|
||||
println!("Plug out monitor begin");
|
||||
if let Err(e) = virtual_display::plug_out_monitor() {
|
||||
if let Err(e) = virtual_display::plug_out_monitor(monitor_index as _) {
|
||||
println!("{}", e);
|
||||
} else {
|
||||
println!("Plug out monitor done");
|
||||
@ -38,8 +38,9 @@ fn plug_out() {
|
||||
|
||||
fn main() {
|
||||
loop {
|
||||
match prompt_input() as char {
|
||||
'x' => break,
|
||||
let chr = prompt_input();
|
||||
match chr as char {
|
||||
'q' => break,
|
||||
'i' => {
|
||||
println!("Install or update driver begin");
|
||||
let mut reboot_required = false;
|
||||
@ -81,8 +82,12 @@ fn main() {
|
||||
virtual_display::close_device();
|
||||
println!("Close device done");
|
||||
}
|
||||
'1' => plug_in(),
|
||||
'4' => plug_out(),
|
||||
'1' => plug_in(0),
|
||||
'2' => plug_in(1),
|
||||
'3' => plug_in(2),
|
||||
'4' => plug_out(0),
|
||||
'5' => plug_out(1),
|
||||
'6' => plug_out(2),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,96 @@
|
||||
use hbb_common::{bail, ResultType};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use hbb_common::{anyhow, dlopen::symbor::Library, log, ResultType};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
const LIB_NAME_VIRTUAL_DISPLAY: &str = "dylib_virtual_display";
|
||||
|
||||
pub type DWORD = ::std::os::raw::c_ulong;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct _MonitorMode {
|
||||
pub width: DWORD,
|
||||
pub height: DWORD,
|
||||
pub sync: DWORD,
|
||||
}
|
||||
pub type MonitorMode = _MonitorMode;
|
||||
pub type PMonitorMode = *mut MonitorMode;
|
||||
|
||||
pub type GetDriverInstallPath = fn() -> &'static str;
|
||||
pub type IsDeviceCreated = fn() -> bool;
|
||||
pub type CloseDevice = fn();
|
||||
pub type DownLoadDriver = fn() -> ResultType<()>;
|
||||
pub type CreateDevice = fn() -> ResultType<()>;
|
||||
pub type InstallUpdateDriver = fn(&mut bool) -> ResultType<()>;
|
||||
pub type UninstallDriver = fn(&mut bool) -> ResultType<()>;
|
||||
pub type PlugInMonitor = fn(u32) -> ResultType<()>;
|
||||
pub type PlugOutMonitor = fn(u32) -> ResultType<()>;
|
||||
pub type UpdateMonitorModes = fn(u32, u32, PMonitorMode) -> ResultType<()>;
|
||||
|
||||
macro_rules! make_lib_wrapper {
|
||||
($($field:ident : $tp:ty),+) => {
|
||||
struct LibWrapper {
|
||||
_lib: Option<Library>,
|
||||
$($field: Option<$tp>),+
|
||||
}
|
||||
|
||||
impl LibWrapper {
|
||||
fn new() -> Self {
|
||||
let lib = match Library::open(get_lib_name()) {
|
||||
Ok(lib) => Some(lib),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
$(let $field = if let Some(lib) = &lib {
|
||||
match unsafe { lib.symbol::<$tp>(stringify!($field)) } {
|
||||
Ok(m) => {
|
||||
log::info!("method found {}", stringify!($field));
|
||||
Some(*m)
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load func {}, {}", stringify!($field), e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};)+
|
||||
|
||||
Self {
|
||||
_lib: lib,
|
||||
$( $field ),+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LibWrapper {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
make_lib_wrapper!(
|
||||
get_driver_install_path: GetDriverInstallPath,
|
||||
is_device_created: IsDeviceCreated,
|
||||
close_device: CloseDevice,
|
||||
download_driver: DownLoadDriver,
|
||||
create_device: CreateDevice,
|
||||
install_update_driver: InstallUpdateDriver,
|
||||
uninstall_driver: UninstallDriver,
|
||||
plug_in_monitor: PlugInMonitor,
|
||||
plug_out_monitor: PlugOutMonitor,
|
||||
update_monitor_modes: UpdateMonitorModes
|
||||
);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref LIB_VIRTUAL_DISPLAY: Arc<Mutex<Result<libloading::Library, libloading::Error>>> = {
|
||||
Arc::new(Mutex::new(unsafe { libloading::Library::new(get_lib_name()) }))
|
||||
};
|
||||
static ref LIB_WRAPPER: Arc<Mutex<LibWrapper>> = Default::default();
|
||||
static ref MONITOR_INDICES: Mutex<HashSet<u32>> = Mutex::new(HashSet::new());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@ -24,102 +108,90 @@ fn get_lib_name() -> String {
|
||||
format!("lib{}.dylib", LIB_NAME_VIRTUAL_DISPLAY)
|
||||
}
|
||||
|
||||
fn try_reload_lib() {
|
||||
let mut lock = LIB_VIRTUAL_DISPLAY.lock().unwrap();
|
||||
if lock.is_err() {
|
||||
*lock = unsafe { libloading::Library::new(get_lib_name()) };
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn get_dirver_install_path() -> ResultType<&'static str> {
|
||||
try_reload_lib();
|
||||
match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() {
|
||||
Ok(lib) => unsafe {
|
||||
match lib.get::<libloading::Symbol<fn() -> &'static str>>(b"get_dirver_install_path") {
|
||||
Ok(func) => Ok(func()),
|
||||
Err(e) => bail!("Failed to load func get_dirver_install_path, {}", e),
|
||||
}
|
||||
},
|
||||
Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e),
|
||||
}
|
||||
pub fn get_driver_install_path() -> Option<&'static str> {
|
||||
Some(LIB_WRAPPER.lock().unwrap().get_driver_install_path?())
|
||||
}
|
||||
|
||||
pub fn is_device_created() -> bool {
|
||||
try_reload_lib();
|
||||
match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() {
|
||||
Ok(lib) => unsafe {
|
||||
match lib.get::<libloading::Symbol<fn() -> bool>>(b"is_device_created") {
|
||||
Ok(func) => func(),
|
||||
Err(..) => false,
|
||||
}
|
||||
},
|
||||
Err(..) => false,
|
||||
}
|
||||
LIB_WRAPPER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.is_device_created
|
||||
.map(|f| f())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn close_device() {
|
||||
try_reload_lib();
|
||||
match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() {
|
||||
Ok(lib) => unsafe {
|
||||
match lib.get::<libloading::Symbol<fn()>>(b"close_device") {
|
||||
Ok(func) => func(),
|
||||
Err(..) => {}
|
||||
}
|
||||
},
|
||||
Err(..) => {}
|
||||
}
|
||||
let _r = LIB_WRAPPER.lock().unwrap().close_device.map(|f| f());
|
||||
}
|
||||
|
||||
macro_rules! def_func_result {
|
||||
($func:ident, $name: tt) => {
|
||||
pub fn $func() -> ResultType<()> {
|
||||
try_reload_lib();
|
||||
match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() {
|
||||
Ok(lib) => unsafe {
|
||||
match lib.get::<libloading::Symbol<fn() -> ResultType<()>>>($name.as_bytes()) {
|
||||
Ok(func) => func(),
|
||||
Err(e) => bail!("Failed to load func {}, {}", $name, e),
|
||||
}
|
||||
},
|
||||
Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e),
|
||||
}
|
||||
}
|
||||
};
|
||||
pub fn download_driver() -> ResultType<()> {
|
||||
LIB_WRAPPER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.download_driver
|
||||
.ok_or(anyhow::Error::msg("download_driver method not found"))?()
|
||||
}
|
||||
|
||||
pub fn create_device() -> ResultType<()> {
|
||||
LIB_WRAPPER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.create_device
|
||||
.ok_or(anyhow::Error::msg("create_device method not found"))?()
|
||||
}
|
||||
|
||||
pub fn install_update_driver(reboot_required: &mut bool) -> ResultType<()> {
|
||||
try_reload_lib();
|
||||
match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() {
|
||||
Ok(lib) => unsafe {
|
||||
match lib.get::<libloading::Symbol<fn(&mut bool) -> ResultType<()>>>(
|
||||
b"install_update_driver",
|
||||
) {
|
||||
Ok(func) => func(reboot_required),
|
||||
Err(e) => bail!("Failed to load func install_update_driver, {}", e),
|
||||
}
|
||||
},
|
||||
Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e),
|
||||
}
|
||||
LIB_WRAPPER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.install_update_driver
|
||||
.ok_or(anyhow::Error::msg("install_update_driver method not found"))?(reboot_required)
|
||||
}
|
||||
|
||||
pub fn uninstall_driver(reboot_required: &mut bool) -> ResultType<()> {
|
||||
try_reload_lib();
|
||||
match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() {
|
||||
Ok(lib) => unsafe {
|
||||
match lib
|
||||
.get::<libloading::Symbol<fn(&mut bool) -> ResultType<()>>>(b"uninstall_driver")
|
||||
{
|
||||
Ok(func) => func(reboot_required),
|
||||
Err(e) => bail!("Failed to load func uninstall_driver, {}", e),
|
||||
}
|
||||
},
|
||||
Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e),
|
||||
}
|
||||
LIB_WRAPPER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.uninstall_driver
|
||||
.ok_or(anyhow::Error::msg("uninstall_driver method not found"))?(reboot_required)
|
||||
}
|
||||
|
||||
def_func_result!(download_driver, "download_driver");
|
||||
def_func_result!(create_device, "create_device");
|
||||
def_func_result!(plug_in_monitor, "plug_in_monitor");
|
||||
def_func_result!(plug_out_monitor, "plug_out_monitor");
|
||||
def_func_result!(update_monitor_modes, "update_monitor_modes");
|
||||
#[cfg(windows)]
|
||||
pub fn plug_in_monitor(monitor_index: u32) -> ResultType<()> {
|
||||
let mut lock = MONITOR_INDICES.lock().unwrap();
|
||||
if lock.contains(&monitor_index) {
|
||||
return Ok(());
|
||||
}
|
||||
let f = LIB_WRAPPER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.plug_in_monitor
|
||||
.ok_or(anyhow::Error::msg("plug_in_monitor method not found"))?;
|
||||
f(monitor_index)?;
|
||||
lock.insert(monitor_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn plug_out_monitor(monitor_index: u32) -> ResultType<()> {
|
||||
let f = LIB_WRAPPER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.plug_out_monitor
|
||||
.ok_or(anyhow::Error::msg("plug_out_monitor method not found"))?;
|
||||
f(monitor_index)?;
|
||||
MONITOR_INDICES.lock().unwrap().remove(&monitor_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn update_monitor_modes(monitor_index: u32, modes: &[MonitorMode]) -> ResultType<()> {
|
||||
let f = LIB_WRAPPER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.update_monitor_modes
|
||||
.ok_or(anyhow::Error::msg("update_monitor_modes method not found"))?;
|
||||
f(monitor_index, modes.len() as _, modes.as_ptr() as _)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ arch=('x86_64')
|
||||
url=""
|
||||
license=('AGPL-3.0')
|
||||
groups=()
|
||||
depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'curl' 'libva' 'libvdpau' 'libappindicator-gtk3')
|
||||
depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'curl' 'libva' 'libvdpau' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire')
|
||||
makedepends=()
|
||||
checkdepends=()
|
||||
optdepends=()
|
||||
|
5
res/pam.d/rustdesk.debian
Normal file
5
res/pam.d/rustdesk.debian
Normal file
@ -0,0 +1,5 @@
|
||||
#%PAM-1.0
|
||||
@include common-auth
|
||||
@include common-account
|
||||
@include common-session
|
||||
@include common-password
|
5
res/pam.d/rustdesk.suse
Normal file
5
res/pam.d/rustdesk.suse
Normal file
@ -0,0 +1,5 @@
|
||||
#%PAM-1.0
|
||||
auth include common-auth
|
||||
account include common-account
|
||||
session include common-session
|
||||
password include common-password
|
@ -3,7 +3,7 @@ Version: 1.2.0
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libappindicator-gtk3 libvdpau1 libva2
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libappindicator-gtk3 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
|
||||
|
||||
%description
|
||||
|
@ -3,7 +3,7 @@ Version: 1.2.0
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator-gtk3 libvdpau libva
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator-gtk3 libvdpau libva pam gstreamer1-plugins-base
|
||||
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
|
||||
|
||||
%description
|
||||
|
@ -3,7 +3,7 @@ Version: 1.1.9
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libayatana-appindicator3-1 libvdpau1 libva2
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
|
||||
%description
|
||||
The best open-source remote desktop client software, written in Rust.
|
||||
|
@ -3,7 +3,7 @@ Version: 1.2.0
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator libvdpau1 libva2
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator libvdpau1 libva2 pam gstreamer1-plugins-base
|
||||
|
||||
%description
|
||||
The best open-source remote desktop client software, written in Rust.
|
||||
|
130
res/startwm.sh
Executable file
130
res/startwm.sh
Executable file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script is derived from https://github.com/neutrinolabs/xrdp/sesman/startwm.sh.
|
||||
|
||||
#
|
||||
# This script is an example. You might need to edit this script
|
||||
# depending on your distro if it doesn't work for you.
|
||||
#
|
||||
# Uncomment the following line for debug:
|
||||
# exec xterm
|
||||
|
||||
|
||||
# Execution sequence for interactive login shell - pseudocode
|
||||
#
|
||||
# IF /etc/profile is readable THEN
|
||||
# execute ~/.bash_profile
|
||||
# END IF
|
||||
# IF ~/.bash_profile is readable THEN
|
||||
# execute ~/.bash_profile
|
||||
# ELSE
|
||||
# IF ~/.bash_login is readable THEN
|
||||
# execute ~/.bash_login
|
||||
# ELSE
|
||||
# IF ~/.profile is readable THEN
|
||||
# execute ~/.profile
|
||||
# END IF
|
||||
# END IF
|
||||
# END IF
|
||||
pre_start()
|
||||
{
|
||||
if [ -r /etc/profile ]; then
|
||||
. /etc/profile
|
||||
fi
|
||||
if [ -r ~/.bash_profile ]; then
|
||||
. ~/.bash_profile
|
||||
else
|
||||
if [ -r ~/.bash_login ]; then
|
||||
. ~/.bash_login
|
||||
else
|
||||
if [ -r ~/.profile ]; then
|
||||
. ~/.profile
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# When loging out from the interactive shell, the execution sequence is:
|
||||
#
|
||||
# IF ~/.bash_logout exists THEN
|
||||
# execute ~/.bash_logout
|
||||
# END IF
|
||||
post_start()
|
||||
{
|
||||
if [ -r ~/.bash_logout ]; then
|
||||
. ~/.bash_logout
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
#start the window manager
|
||||
wm_start()
|
||||
{
|
||||
if [ -r /etc/default/locale ]; then
|
||||
. /etc/default/locale
|
||||
export LANG LANGUAGE
|
||||
fi
|
||||
|
||||
# debian
|
||||
if [ -r /etc/X11/Xsession ]; then
|
||||
pre_start
|
||||
. /etc/X11/Xsession
|
||||
post_start
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# alpine
|
||||
# Don't use /etc/X11/xinit/Xsession - it doesn't work
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
if [ -f /etc/X11/xinit/xinitrc ]; then
|
||||
pre_start
|
||||
/etc/X11/xinit/xinitrc
|
||||
post_start
|
||||
else
|
||||
echo "** xinit package isn't installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# el
|
||||
if [ -r /etc/X11/xinit/Xsession ]; then
|
||||
pre_start
|
||||
. /etc/X11/xinit/Xsession
|
||||
post_start
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# suse
|
||||
if [ -r /etc/X11/xdm/Xsession ]; then
|
||||
# since the following script run a user login shell,
|
||||
# do not execute the pseudo login shell scripts
|
||||
. /etc/X11/xdm/Xsession
|
||||
exit 0
|
||||
elif [ -r /usr/etc/X11/xdm/Xsession ]; then
|
||||
. /usr/etc/X11/xdm/Xsession
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pre_start
|
||||
xterm
|
||||
post_start
|
||||
}
|
||||
|
||||
#. /etc/environment
|
||||
#export PATH=$PATH
|
||||
#export LANG=$LANG
|
||||
|
||||
# change PATH to be what your environment needs usually what is in
|
||||
# /etc/environment
|
||||
#PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games"
|
||||
#export PATH=$PATH
|
||||
|
||||
# for PATH and LANG from /etc/environment
|
||||
# pam will auto process the environment file if /etc/pam.d/xrdp-sesman
|
||||
# includes
|
||||
# auth required pam_env.so readenv=1
|
||||
|
||||
wm_start
|
||||
|
||||
exit 1
|
30
res/xorg.conf
Normal file
30
res/xorg.conf
Normal file
@ -0,0 +1,30 @@
|
||||
Section "Monitor"
|
||||
Identifier "Dummy Monitor"
|
||||
|
||||
# Default HorizSync 31.50 - 48.00 kHz
|
||||
HorizSync 5.0 - 150.0
|
||||
# Default VertRefresh 50.00 - 70.00 Hz
|
||||
VertRefresh 5.0 - 100.0
|
||||
|
||||
# Taken from https://www.xpra.org/xorg.conf
|
||||
Modeline "1920x1080" 23.53 1920 1952 2040 2072 1080 1106 1108 1135
|
||||
Modeline "1280x720" 27.41 1280 1312 1416 1448 720 737 740 757
|
||||
EndSection
|
||||
|
||||
Section "Device"
|
||||
Identifier "Dummy VideoCard"
|
||||
Driver "dummy"
|
||||
# Default VideoRam 4096
|
||||
# (1920 * 1080 * 4) / 1024 = 8100
|
||||
VideoRam 8100
|
||||
EndSection
|
||||
|
||||
Section "Screen"
|
||||
Identifier "Dummy Screen"
|
||||
Device "Dummy VideoCard"
|
||||
Monitor "Dummy Monitor"
|
||||
SubSectionSub "Display"
|
||||
Depth 24
|
||||
Modes "1920x1080" "1280x720"
|
||||
EndSubSection
|
||||
EndSection
|
87
src/api.rs
Normal file
87
src/api.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use std::ffi::{c_char};
|
||||
|
||||
use crate::{
|
||||
flutter::{FlutterHandler, SESSIONS},
|
||||
plugins::PLUGIN_REGISTRAR,
|
||||
ui_session_interface::Session,
|
||||
};
|
||||
|
||||
// API provided by RustDesk.
|
||||
pub type LoadPluginFunc = fn(*const c_char) -> i32;
|
||||
pub type UnloadPluginFunc = fn(*const c_char) -> i32;
|
||||
pub type AddSessionFunc = fn(session_id: String) -> bool;
|
||||
pub type RemoveSessionFunc = fn(session_id: &String) -> bool;
|
||||
pub type AddSessionHookFunc = fn(session_id: String, key: String, hook: SessionHook) -> bool;
|
||||
pub type RemoveSessionHookFunc = fn(session_id: String, key: &String) -> bool;
|
||||
|
||||
/// Hooks for session.
|
||||
#[derive(Clone)]
|
||||
pub enum SessionHook {
|
||||
OnSessionRgba(fn(String, Vec<i8>) -> Vec<i8>),
|
||||
}
|
||||
|
||||
// #[repr(C)]
|
||||
pub struct RustDeskApiTable {
|
||||
pub(crate) load_plugin: LoadPluginFunc,
|
||||
pub(crate) unload_plugin: UnloadPluginFunc,
|
||||
pub add_session: AddSessionFunc,
|
||||
pub remove_session: RemoveSessionFunc,
|
||||
pub add_session_hook: AddSessionHookFunc,
|
||||
pub remove_session_hook: RemoveSessionHookFunc,
|
||||
}
|
||||
|
||||
fn load_plugin(path: *const c_char) -> i32 {
|
||||
PLUGIN_REGISTRAR.load_plugin(path)
|
||||
}
|
||||
|
||||
fn unload_plugin(path: *const c_char) -> i32 {
|
||||
PLUGIN_REGISTRAR.unload_plugin(path)
|
||||
}
|
||||
|
||||
fn add_session(session_id: String) -> bool {
|
||||
// let mut sessions = SESSIONS.write().unwrap();
|
||||
// if sessions.contains_key(&session.id) {
|
||||
// return false;
|
||||
// }
|
||||
// let _ = sessions.insert(session.id.to_owned(), session);
|
||||
// true
|
||||
false
|
||||
}
|
||||
|
||||
fn remove_session(session_id: &String) -> bool {
|
||||
let mut sessions = SESSIONS.write().unwrap();
|
||||
if !sessions.contains_key(session_id) {
|
||||
return false;
|
||||
}
|
||||
let _ = sessions.remove(session_id);
|
||||
true
|
||||
}
|
||||
|
||||
fn add_session_hook(session_id: String, key: String, hook: SessionHook) -> bool {
|
||||
let sessions = SESSIONS.read().unwrap();
|
||||
if let Some(session) = sessions.get(&session_id) {
|
||||
return session.add_session_hook(key, hook);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn remove_session_hook(session_id: String, key: &String) -> bool {
|
||||
let sessions = SESSIONS.read().unwrap();
|
||||
if let Some(session) = sessions.get(&session_id) {
|
||||
return session.remove_session_hook(key);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
impl Default for RustDeskApiTable {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
load_plugin,
|
||||
unload_plugin,
|
||||
add_session,
|
||||
remove_session,
|
||||
add_session_hook,
|
||||
remove_session_hook,
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user