Merge branch 'master' into master

This commit is contained in:
RustDesk 2023-04-20 23:28:45 +08:00 committed by GitHub
commit e967294f1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
198 changed files with 14433 additions and 7966 deletions

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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

File diff suppressed because it is too large Load Diff

370
.github/workflows/history.yml vendored Normal file
View 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
View 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
View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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)

View File

@ -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
View 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
View 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
View 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

View File

@ -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
View 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.

View 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

View 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 _;
}

View File

@ -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

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>

View File

@ -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'

View File

@ -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

View File

@ -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;

View File

@ -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("", "");
}
}

View File

@ -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>

View File

@ -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*);

View File

@ -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, its 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"));
}
}

View File

@ -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');
}

View File

@ -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]);
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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,
),
],

View 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;
}

View File

@ -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(

View File

@ -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');

View File

@ -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,

View File

@ -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();

View File

@ -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);
}

View File

@ -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(

View 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());
}
}

View 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];
}

View 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
}
}
}

View 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]!;
}

View 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

View File

@ -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

View File

@ -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();
});
}

View File

@ -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);

View File

@ -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) {

View File

@ -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);
}

View File

@ -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;
});
},
),
),
);
}
}

View File

@ -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')) {

View File

@ -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;

View File

@ -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';

View File

@ -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"

View File

@ -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

View File

@ -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()

View File

@ -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 {

View File

@ -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

View File

@ -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 _ }
}
}

View File

@ -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"

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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())
}

View File

@ -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,

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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();

View File

@ -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.

View File

@ -50,6 +50,7 @@ impl crate::TraitCapturer for Capturer {
pub enum Frame<'a> {
RAW(&'a [u8]),
VP8(&'a [u8]),
VP9(&'a [u8]),
Empty,
}

View File

@ -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" {

View File

@ -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();
}
}

View File

@ -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(),
}
}
}

View File

@ -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"))?;

View File

@ -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 {

View File

@ -1,3 +1,3 @@
pub mod capturable;
pub mod pipewire;
mod pipewire_dbus;
pub mod capturable;

View File

@ -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" }

View File

@ -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();
}
}

View File

@ -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"

View File

@ -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();
// }
}

View File

@ -7,5 +7,4 @@ edition = "2021"
[dependencies]
lazy_static = "1.4"
libloading = "0.7"
hbb_common = { path = "../hbb_common" }
hbb_common = { path = "../hbb_common" }

View File

@ -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()?);
}

View File

@ -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);

View File

@ -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),
_ => {}
}
}

View File

@ -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 _)
}

View File

@ -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=()

View 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
View File

@ -0,0 +1,5 @@
#%PAM-1.0
auth include common-auth
account include common-account
session include common-session
password include common-password

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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
View 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
View 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
View 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