Compare commits
196 Commits
34
.github/workflows/fdroid.yml
vendored
Normal file
34
.github/workflows/fdroid.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Fdroid version file generation
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml
|
||||||
|
# Finds latest release and transforms F-Droid version code from version as follows:
|
||||||
|
# X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A
|
||||||
|
update-fdroid-version-file:
|
||||||
|
name: Publish RustDesk version file for F-Droid updater
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Generate RustDesk version file
|
||||||
|
run: |
|
||||||
|
UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')"
|
||||||
|
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)"
|
||||||
|
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
||||||
|
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Publish RustDesk version file
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: "fdroid-version"
|
||||||
|
files: |
|
||||||
|
./rustdesk-version.txt
|
||||||
120
.github/workflows/flutter-build.yml
vendored
120
.github/workflows/flutter-build.yml
vendored
@@ -10,6 +10,13 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
default: "nightly"
|
default: "nightly"
|
||||||
|
|
||||||
|
# NOTE: F-Droid builder script 'flutter/build_fdroid.sh' reads environment
|
||||||
|
# variables from this workflow!
|
||||||
|
#
|
||||||
|
# It does NOT read build steps, however, so please fix 'flutter/build_fdroid.sh
|
||||||
|
# whenever you add changes to Android CI build action ('build-rustdesk-android')
|
||||||
|
# in this file!
|
||||||
|
|
||||||
env:
|
env:
|
||||||
WIN_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
WIN_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||||
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
|
||||||
@@ -22,9 +29,9 @@ env:
|
|||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.03.25
|
# vcpkg version: 2024.06.15
|
||||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||||
VERSION: "1.2.6"
|
VERSION: "1.2.7"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -58,7 +65,7 @@ jobs:
|
|||||||
job:
|
job:
|
||||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||||
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64 }
|
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, vcpkg-triplet: x64-windows-static }
|
||||||
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
|
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -109,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
run: |
|
run: |
|
||||||
$VCPKG_ROOT/vcpkg install --triplet x64-windows-static --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Build rustdesk
|
- name: Build rustdesk
|
||||||
@@ -163,6 +170,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
run: |
|
run: |
|
||||||
|
sed -i '/dpiAware/d' res/manifest.xml
|
||||||
pushd ./libs/portable
|
pushd ./libs/portable
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
python3 ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe
|
python3 ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe
|
||||||
@@ -211,7 +219,7 @@ jobs:
|
|||||||
job:
|
job:
|
||||||
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||||
- { target: i686-pc-windows-msvc, os: windows-2022, arch: x86 }
|
- { target: i686-pc-windows-msvc, os: windows-2022, arch: x86, vcpkg-triplet: x86-windows-static }
|
||||||
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
|
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -248,7 +256,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
run: |
|
run: |
|
||||||
$VCPKG_ROOT/vcpkg install --triplet x86-windows-static --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Build rustdesk
|
- name: Build rustdesk
|
||||||
@@ -297,14 +305,13 @@ jobs:
|
|||||||
- name: Build self-extracted executable
|
- name: Build self-extracted executable
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
sed -i '/dpiAware/d' res/manifest.xml
|
||||||
pushd ./libs/portable
|
pushd ./libs/portable
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
python3 ./generate.py -f ../../Release/ -o . -e ../../Release/rustdesk.exe
|
python3 ./generate.py -f ../../Release/ -o . -e ../../Release/rustdesk.exe
|
||||||
popd
|
popd
|
||||||
mkdir -p ./SignOutput
|
mkdir -p ./SignOutput
|
||||||
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
|
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
|
||||||
mv ./Release ./rustdesk
|
|
||||||
tar czf rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.tar.gz rustdesk
|
|
||||||
|
|
||||||
- name: Sign rustdesk self-extracted file
|
- name: Sign rustdesk self-extracted file
|
||||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
||||||
@@ -320,7 +327,6 @@ jobs:
|
|||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
files: |
|
files: |
|
||||||
./SignOutput/rustdesk-*.exe
|
./SignOutput/rustdesk-*.exe
|
||||||
./rustdesk-*.tar.gz
|
|
||||||
|
|
||||||
build-for-macOS-arm64-selfhost:
|
build-for-macOS-arm64-selfhost:
|
||||||
# use build-for-macOS instead
|
# use build-for-macOS instead
|
||||||
@@ -406,6 +412,7 @@ jobs:
|
|||||||
arch: aarch64,
|
arch: aarch64,
|
||||||
target: aarch64-apple-ios,
|
target: aarch64-apple-ios,
|
||||||
os: macos-13,
|
os: macos-13,
|
||||||
|
vcpkg-triplet: arm64-ios,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -433,7 +440,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
run: |
|
run: |
|
||||||
$VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
@@ -614,6 +621,14 @@ jobs:
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
|
||||||
|
- name: Workaround for flutter issue
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd "$(dirname "$(which flutter)")"
|
||||||
|
# https://github.com/flutter/flutter/issues/133533
|
||||||
|
sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart
|
||||||
|
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -966,12 +981,16 @@ jobs:
|
|||||||
target: x86_64-unknown-linux-gnu,
|
target: x86_64-unknown-linux-gnu,
|
||||||
distro: ubuntu18.04,
|
distro: ubuntu18.04,
|
||||||
on: ubuntu-20.04,
|
on: ubuntu-20.04,
|
||||||
|
deb_arch: amd64,
|
||||||
|
vcpkg-triplet: x64-linux,
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
arch: aarch64,
|
arch: aarch64,
|
||||||
target: aarch64-unknown-linux-gnu,
|
target: aarch64-unknown-linux-gnu,
|
||||||
distro: ubuntu18.04,
|
distro: ubuntu18.04,
|
||||||
on: [self-hosted, Linux, ARM64],
|
on: [self-hosted, Linux, ARM64],
|
||||||
|
deb_arch: arm64,
|
||||||
|
vcpkg-triplet: arm64-linux,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -1006,6 +1025,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
@@ -1022,31 +1042,27 @@ jobs:
|
|||||||
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
|
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@master
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@v11
|
||||||
with:
|
with:
|
||||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
run: |
|
run: |
|
||||||
case ${{ matrix.job.target }} in
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
aarch64-unknown-linux-gnu)
|
|
||||||
$VCPKG_ROOT/vcpkg install --triplet arm64-linux --x-install-root="$VCPKG_ROOT/installed"
|
|
||||||
;;
|
|
||||||
x86_64-unknown-linux-gnu)
|
|
||||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@master
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
@@ -1055,7 +1071,7 @@ jobs:
|
|||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||||
name: Build rustdesk
|
name: Build rustdesk
|
||||||
id: vcpkg
|
id: vcpkg
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.job.arch }}
|
arch: ${{ matrix.job.arch }}
|
||||||
distro: ${{ matrix.job.distro }}
|
distro: ${{ matrix.job.distro }}
|
||||||
@@ -1146,7 +1162,6 @@ jobs:
|
|||||||
aarch64)
|
aarch64)
|
||||||
export PATH=/opt/flutter-elinux/bin:$PATH
|
export PATH=/opt/flutter-elinux/bin:$PATH
|
||||||
sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py
|
sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py
|
||||||
export ARCH=arm64
|
|
||||||
sed -i "s/x64\/release/arm64\/release/g" ./build.py
|
sed -i "s/x64\/release/arm64\/release/g" ./build.py
|
||||||
;;
|
;;
|
||||||
x86_64)
|
x86_64)
|
||||||
@@ -1178,6 +1193,7 @@ jobs:
|
|||||||
# build flutter
|
# build flutter
|
||||||
pushd /workspace
|
pushd /workspace
|
||||||
export CARGO_INCREMENTAL=0
|
export CARGO_INCREMENTAL=0
|
||||||
|
export DEB_ARCH=${{ matrix.job.deb_arch }}
|
||||||
python3 ./build.py --flutter --skip-cargo
|
python3 ./build.py --flutter --skip-cargo
|
||||||
for name in rustdesk*??.deb; do
|
for name in rustdesk*??.deb; do
|
||||||
mv "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb"
|
mv "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb"
|
||||||
@@ -1256,21 +1272,33 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
res/rustdesk-${{ env.VERSION }}*.zst
|
res/rustdesk-${{ env.VERSION }}*.zst
|
||||||
|
|
||||||
build-rustdesk-sciter-arm:
|
build-rustdesk-linux-sciter:
|
||||||
if: ${{ inputs.upload-artifact }}
|
if: ${{ inputs.upload-artifact }}
|
||||||
needs: build-rustdesk-linux # not for dep, just make it run later for parallelism
|
needs: build-rustdesk-linux # not for dep, just make it run later for parallelism
|
||||||
runs-on: [self-hosted, Linux, ARM64]
|
runs-on: ${{ matrix.job.on }}
|
||||||
name: build-rustdesk-sciter-arm ${{ matrix.job.target }}
|
name: build-rustdesk-linux-sciter ${{ matrix.job.target }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# use a high level qemu-user-static
|
# use a high level qemu-user-static
|
||||||
job:
|
job:
|
||||||
|
- {
|
||||||
|
arch: x86_64,
|
||||||
|
target: x86_64-unknown-linux-gnu,
|
||||||
|
on: ubuntu-20.04,
|
||||||
|
distro: ubuntu18.04,
|
||||||
|
deb_arch: amd64,
|
||||||
|
sciter_arch: x64,
|
||||||
|
vcpkg-triplet: x64-linux,
|
||||||
|
}
|
||||||
- {
|
- {
|
||||||
arch: armv7,
|
arch: armv7,
|
||||||
target: armv7-unknown-linux-gnueabihf,
|
target: armv7-unknown-linux-gnueabihf,
|
||||||
deb-arch: armhf,
|
on: [self-hosted, Linux, ARM64],
|
||||||
use-cross: true,
|
distro: ubuntu18.04-rustdesk,
|
||||||
|
deb_arch: armhf,
|
||||||
|
sciter_arch: arm32,
|
||||||
|
vcpkg-triplet: arm-linux,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
@@ -1326,7 +1354,7 @@ jobs:
|
|||||||
- name: Install vcpkg dependencies
|
- name: Install vcpkg dependencies
|
||||||
run: |
|
run: |
|
||||||
cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake
|
cp $PWD/res/vcpkg/linux.cmake $VCPKG_ROOT/scripts/toolchains/linux.cmake
|
||||||
$VCPKG_ROOT/vcpkg install --triplet arm-linux --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||||
@@ -1334,7 +1362,7 @@ jobs:
|
|||||||
id: vcpkg
|
id: vcpkg
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.job.arch }}
|
arch: ${{ matrix.job.arch }}
|
||||||
distro: ubuntu18.04-rustdesk
|
distro: ${{ matrix.job.distro }}
|
||||||
githubToken: ${{ github.token }}
|
githubToken: ${{ github.token }}
|
||||||
setup: |
|
setup: |
|
||||||
ls -l "${PWD}"
|
ls -l "${PWD}"
|
||||||
@@ -1400,13 +1428,13 @@ jobs:
|
|||||||
pushd /workspace
|
pushd /workspace
|
||||||
python3 ./res/inline-sciter.py
|
python3 ./res/inline-sciter.py
|
||||||
export VCPKG_ROOT=/opt/artifacts/vcpkg
|
export VCPKG_ROOT=/opt/artifacts/vcpkg
|
||||||
export ARCH=armhf
|
|
||||||
export CARGO_INCREMENTAL=0
|
export CARGO_INCREMENTAL=0
|
||||||
cargo build --features inline --release --bins --jobs 1
|
cargo build --features inline --release --bins --jobs 1
|
||||||
# package
|
# package
|
||||||
mkdir -p ./Release
|
mkdir -p ./Release
|
||||||
mv ./target/release/rustdesk ./Release/rustdesk
|
mv ./target/release/rustdesk ./Release/rustdesk
|
||||||
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/arm32/libsciter-gtk.so
|
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so
|
||||||
|
export DEB_ARCH=${{ matrix.job.deb_arch }}
|
||||||
./build.py --package ./Release
|
./build.py --package ./Release
|
||||||
|
|
||||||
- name: Rename rustdesk
|
- name: Rename rustdesk
|
||||||
@@ -1426,6 +1454,13 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||||
|
|
||||||
|
- name: Upload deb
|
||||||
|
uses: actions/upload-artifact@master
|
||||||
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
|
with:
|
||||||
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||||
|
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||||
|
|
||||||
build-appimage:
|
build-appimage:
|
||||||
name: Build appimage ${{ matrix.job.target }}
|
name: Build appimage ${{ matrix.job.target }}
|
||||||
needs: [build-rustdesk-linux]
|
needs: [build-rustdesk-linux]
|
||||||
@@ -1483,8 +1518,10 @@ jobs:
|
|||||||
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
|
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
|
||||||
|
|
||||||
build-flatpak:
|
build-flatpak:
|
||||||
name: Build flatpak ${{ matrix.job.target }}
|
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
|
||||||
needs: [build-rustdesk-linux]
|
needs:
|
||||||
|
- build-rustdesk-linux
|
||||||
|
- build-rustdesk-linux-sciter
|
||||||
runs-on: ${{ matrix.job.on }}
|
runs-on: ${{ matrix.job.on }}
|
||||||
if: ${{ inputs.upload-artifact }}
|
if: ${{ inputs.upload-artifact }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -1496,6 +1533,14 @@ jobs:
|
|||||||
distro: ubuntu18.04,
|
distro: ubuntu18.04,
|
||||||
on: ubuntu-20.04,
|
on: ubuntu-20.04,
|
||||||
arch: x86_64,
|
arch: x86_64,
|
||||||
|
suffix: "",
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-unknown-linux-gnu,
|
||||||
|
distro: ubuntu18.04,
|
||||||
|
on: ubuntu-20.04,
|
||||||
|
arch: x86_64,
|
||||||
|
suffix: "-sciter",
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
target: aarch64-unknown-linux-gnu,
|
target: aarch64-unknown-linux-gnu,
|
||||||
@@ -1503,6 +1548,7 @@ jobs:
|
|||||||
distro: ubuntu22.04,
|
distro: ubuntu22.04,
|
||||||
on: [self-hosted, Linux, ARM64],
|
on: [self-hosted, Linux, ARM64],
|
||||||
arch: aarch64,
|
arch: aarch64,
|
||||||
|
suffix: "",
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
@@ -1511,12 +1557,12 @@ jobs:
|
|||||||
- name: Download Binary
|
- name: Download Binary
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@master
|
||||||
with:
|
with:
|
||||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb
|
||||||
path: .
|
path: .
|
||||||
|
|
||||||
- name: Rename Binary
|
- name: Rename Binary
|
||||||
run: |
|
run: |
|
||||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb flatpak/rustdesk.deb
|
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||||
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
||||||
@@ -1562,7 +1608,7 @@ jobs:
|
|||||||
pushd flatpak
|
pushd flatpak
|
||||||
git clone https://github.com/flathub/shared-modules.git --depth=1
|
git clone https://github.com/flathub/shared-modules.git --depth=1
|
||||||
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
||||||
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
|
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
|
||||||
|
|
||||||
- name: Publish flatpak package
|
- name: Publish flatpak package
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
@@ -1570,7 +1616,7 @@ jobs:
|
|||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
files: |
|
files: |
|
||||||
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
|
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak
|
||||||
|
|
||||||
build-rustdesk-web:
|
build-rustdesk-web:
|
||||||
if: False
|
if: False
|
||||||
|
|||||||
208
.github/workflows/playground.yml
vendored
208
.github/workflows/playground.yml
vendored
@@ -10,15 +10,15 @@ env:
|
|||||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||||
CARGO_NDK_VERSION: "3.1.2"
|
CARGO_NDK_VERSION: "3.1.2"
|
||||||
LLVM_VERSION: "15.0.6"
|
LLVM_VERSION: "15.0.6"
|
||||||
FLUTTER_VERSION: "3.13.9"
|
FLUTTER_VERSION: "3.22.2"
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
# for arm64 linux because official Dart SDK does not work
|
# for arm64 linux because official Dart SDK does not work
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "nightly"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2024.03.25
|
# vcpkg version: 2024.06.15
|
||||||
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
|
||||||
VERSION: "1.2.6"
|
VERSION: "1.2.7"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -31,7 +31,207 @@ env:
|
|||||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build-for-macOS:
|
||||||
|
name: ${{ matrix.job.target }}
|
||||||
|
runs-on: ${{ matrix.job.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.13.9",
|
||||||
|
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231219"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.10.6",
|
||||||
|
ref: "f6509e3fd6917aa976bad2fc684182601ebf2434",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231219"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.10.6",
|
||||||
|
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231119"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||||
|
extra-build-args: "",
|
||||||
|
arch: x86_64,
|
||||||
|
flutter: "3.13.9",
|
||||||
|
ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8",
|
||||||
|
bridge: "1.80.1",
|
||||||
|
date: "20231119"
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Export GitHub Actions cache environment variables
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.job.ref }}
|
||||||
|
|
||||||
|
- name: Import the codesign cert
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
uses: apple-actions/import-codesign-certs@v1
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
||||||
|
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
||||||
|
keychain: rustdesk
|
||||||
|
|
||||||
|
- name: Check sign and import sign key
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
run: |
|
||||||
|
security default-keychain -s rustdesk.keychain
|
||||||
|
security find-identity -v
|
||||||
|
|
||||||
|
- name: Import notarize key
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
uses: timheuer/base64-to-file@v1.2
|
||||||
|
with:
|
||||||
|
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
|
||||||
|
fileName: rustdesk.json
|
||||||
|
fileDir: ${{ github.workspace }}
|
||||||
|
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
|
||||||
|
|
||||||
|
- name: Install rcodesign tool
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pushd /tmp
|
||||||
|
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
|
||||||
|
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
|
||||||
|
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Install build runtime
|
||||||
|
run: |
|
||||||
|
brew install llvm create-dmg nasm cmake gcc wget ninja pkg-config
|
||||||
|
|
||||||
|
- name: Install flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
flutter-version: ${{ matrix.job.flutter }}
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
|
targets: ${{ matrix.job.target }}
|
||||||
|
components: "rustfmt"
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: ${{ matrix.job.os }}
|
||||||
|
|
||||||
|
- name: Install flutter rust bridge deps
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml;
|
||||||
|
cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid"
|
||||||
|
# below works for mac to make buildable on 3.13.9
|
||||||
|
# pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; popd;
|
||||||
|
pushd flutter && flutter pub get && popd
|
||||||
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||||
|
|
||||||
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
|
uses: lukka/run-vcpkg@v11
|
||||||
|
with:
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
run: |
|
||||||
|
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||||
|
|
||||||
|
- name: Restore from cache and install vcpkg
|
||||||
|
uses: lukka/run-vcpkg@v7
|
||||||
|
if: false
|
||||||
|
with:
|
||||||
|
setupOnly: true
|
||||||
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
|
- name: Install vcpkg dependencies
|
||||||
|
if: false
|
||||||
|
run: |
|
||||||
|
$VCPKG_ROOT/vcpkg install libvpx libyuv opus aom
|
||||||
|
|
||||||
|
- 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: |
|
||||||
|
./build.py --flutter ${{ matrix.job.extra-build-args }}
|
||||||
|
|
||||||
|
- name: create unsigned dmg
|
||||||
|
run: |
|
||||||
|
CREATE_DMG="$(command -v create-dmg)"
|
||||||
|
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||||
|
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||||
|
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||||
|
|
||||||
|
- name: Codesign app and create signed dmg
|
||||||
|
if: env.MACOS_P12_BASE64 != null
|
||||||
|
run: |
|
||||||
|
# Patch create-dmg to give more attempts to unmount image
|
||||||
|
CREATE_DMG="$(command -v create-dmg)"
|
||||||
|
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
|
||||||
|
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
|
||||||
|
# Unlock keychain
|
||||||
|
security default-keychain -s rustdesk.keychain
|
||||||
|
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
|
||||||
|
# start sign the rustdesk.app and dmg
|
||||||
|
rm -rf *.dmg || true
|
||||||
|
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
|
||||||
|
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
|
||||||
|
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
|
||||||
|
# notarize the rustdesk-${{ env.VERSION }}.dmg
|
||||||
|
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
|
||||||
|
|
||||||
|
- name: Rename rustdesk
|
||||||
|
run: |
|
||||||
|
for name in rustdesk*??.dmg; do
|
||||||
|
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Publish DMG package
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
rustdesk*-${{ matrix.job.arch }}*.dmg
|
||||||
|
|
||||||
|
|
||||||
build-rustdesk-android:
|
build-rustdesk-android:
|
||||||
|
if: false
|
||||||
name: build rustdesk android apk ${{ matrix.job.target }}
|
name: build rustdesk android apk ${{ matrix.job.target }}
|
||||||
runs-on: ${{ matrix.job.os }}
|
runs-on: ${{ matrix.job.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
2
.github/workflows/winget.yml
vendored
2
.github/workflows/winget.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
|||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
with:
|
with:
|
||||||
identifier: RustDesk.RustDesk
|
identifier: RustDesk.RustDesk
|
||||||
version: ${{ github.event.release.tag_name }}
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
|||||||
2942
Cargo.lock
generated
2942
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.6"
|
version = "1.2.7"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -66,7 +66,7 @@ default-net = "0.14"
|
|||||||
wol-rs = "1.0"
|
wol-rs = "1.0"
|
||||||
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true}
|
||||||
errno = "0.3"
|
errno = "0.3"
|
||||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
rdev = { git = "https://github.com/rustdesk-org/rdev" }
|
||||||
url = { version = "2.3", features = ["serde"] }
|
url = { version = "2.3", features = ["serde"] }
|
||||||
crossbeam-queue = "0.3"
|
crossbeam-queue = "0.3"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
@@ -89,7 +89,10 @@ sys-locale = "0.3"
|
|||||||
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
enigo = { path = "libs/enigo", features = [ "with_serde" ] }
|
||||||
clipboard = { path = "libs/clipboard" }
|
clipboard = { path = "libs/clipboard" }
|
||||||
ctrlc = "3.2"
|
ctrlc = "3.2"
|
||||||
arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/x11_set_conn_timeout", features = ["wayland-data-control"] }
|
# arboard = { version = "3.4.0", features = ["wayland-data-control"] }
|
||||||
|
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] }
|
||||||
|
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||||
|
|
||||||
system_shutdown = "4.0"
|
system_shutdown = "4.0"
|
||||||
qrcode-generator = "4.1"
|
qrcode-generator = "4.1"
|
||||||
|
|
||||||
@@ -149,11 +152,11 @@ psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
|||||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||||
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" }
|
||||||
async-process = "1.7"
|
async-process = "1.7"
|
||||||
mouce = { git="https://github.com/fufesou/mouce.git" }
|
mouce = { git="https://github.com/rustdesk-org/mouce.git" }
|
||||||
evdev = { git="https://github.com/fufesou/evdev" }
|
evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||||
dbus = "0.9"
|
dbus = "0.9"
|
||||||
dbus-crossroads = "0.5"
|
dbus-crossroads = "0.5"
|
||||||
pam = { git="https://github.com/fufesou/pam" }
|
pam = { git="https://github.com/rustdesk-org/pam" }
|
||||||
users = { version = "0.11" }
|
users = { version = "0.11" }
|
||||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.6
|
version: 1.2.7
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.6
|
version: 1.2.7
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
19
build.py
19
build.py
@@ -25,8 +25,8 @@ flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
|
|||||||
skip_cargo = False
|
skip_cargo = False
|
||||||
|
|
||||||
|
|
||||||
def get_arch() -> str:
|
def get_deb_arch() -> str:
|
||||||
custom_arch = os.environ.get("ARCH")
|
custom_arch = os.environ.get("DEB_ARCH")
|
||||||
if custom_arch is None:
|
if custom_arch is None:
|
||||||
return "amd64"
|
return "amd64"
|
||||||
return custom_arch
|
return custom_arch
|
||||||
@@ -48,15 +48,7 @@ def get_version():
|
|||||||
|
|
||||||
|
|
||||||
def parse_rc_features(feature):
|
def parse_rc_features(feature):
|
||||||
available_features = {
|
available_features = {}
|
||||||
'PrivacyMode': {
|
|
||||||
'platform': ['windows'],
|
|
||||||
'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3'
|
|
||||||
'/TempTopMostWindow_x64.zip',
|
|
||||||
'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.3/checksum_md5',
|
|
||||||
'include': ['WindowInjection.dll'],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
apply_features = {}
|
apply_features = {}
|
||||||
if not feature:
|
if not feature:
|
||||||
feature = []
|
feature = []
|
||||||
@@ -81,7 +73,6 @@ def parse_rc_features(feature):
|
|||||||
elif isinstance(feature, list):
|
elif isinstance(feature, list):
|
||||||
if windows:
|
if windows:
|
||||||
# download third party is deprecated, we use github ci instead.
|
# download third party is deprecated, we use github ci instead.
|
||||||
# force add PrivacyMode
|
|
||||||
# feature.append('PrivacyMode')
|
# feature.append('PrivacyMode')
|
||||||
pass
|
pass
|
||||||
for feat in feature:
|
for feat in feature:
|
||||||
@@ -108,7 +99,7 @@ def make_parser():
|
|||||||
nargs='+',
|
nargs='+',
|
||||||
default='',
|
default='',
|
||||||
help='Integrate features, windows only.'
|
help='Integrate features, windows only.'
|
||||||
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
|
'Available: [Not used for now]. Special value is "ALL" and empty "". Default is empty.')
|
||||||
parser.add_argument('--flutter', action='store_true',
|
parser.add_argument('--flutter', action='store_true',
|
||||||
help='Build flutter package', default=False)
|
help='Build flutter package', default=False)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -294,7 +285,7 @@ Homepage: https://rustdesk.com
|
|||||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
|
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
|
||||||
Description: A remote control software.
|
Description: A remote control software.
|
||||||
|
|
||||||
""" % (version, get_arch())
|
""" % (version, get_deb_arch())
|
||||||
file = open(control_file_path, "w")
|
file = open(control_file_path, "w")
|
||||||
file.write(content)
|
file.write(content)
|
||||||
file.close()
|
file.close()
|
||||||
|
|||||||
@@ -1,60 +1,73 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
<img src="../res/logo-header.svg" alt="RustDesk - あなたのためのリモートデスクトップ"><br>
|
||||||
<a href="#free-public-servers">Servers</a> •
|
<a href="#free-public-servers">Servers</a> •
|
||||||
<a href="#raw-steps-to-build">Build</a> •
|
<a href="#raw-steps-to-build">Build</a> •
|
||||||
<a href="#how-to-build-with-docker">Docker</a> •
|
<a href="#how-to-build-with-docker">Docker</a> •
|
||||||
<a href="#file-structure">Structure</a> •
|
<a href="#file-structure">Structure</a> •
|
||||||
<a href="#snapshot">Snapshot</a><br>
|
<a href="#snapshot">Snapshot</a><br>
|
||||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
|
||||||
<b>このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。</b>
|
<b>READMEや<a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a>、 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a>の翻訳者を歓迎します!</b>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
私たちと話す: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||||
|
|
||||||
|
|
||||||
[](https://ko-fi.com/I2I04VU09)
|
[](https://ko-fi.com/I2I04VU09)
|
||||||
|
|
||||||
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。
|
Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分でサーバーをセットアップする](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを作成する](https://github.com/rustdesk/rustdesk-server-demo)こともできます。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。
|
RustDeskは皆さんの貢献を歓迎します。
|
||||||
|
貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。
|
||||||
|
|
||||||
[**RustDeskはどの様に動くのか?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
[**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||||
|
|
||||||
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
[**パッケージのダウンロード**](https://github.com/rustdesk/rustdesk/releases)
|
||||||
|
|
||||||
|
[**ナイトリービルド**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||||
|
|
||||||
|
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||||
|
alt="F-Droidで入手する"
|
||||||
|
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||||
|
|
||||||
## 依存関係
|
## 依存関係
|
||||||
|
|
||||||
デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。
|
デスクトップ版ではGUIにFlutterまたはSciter(非推奨)を使用しますが、チュートリアルでは分かりやすく、簡単なSciterのみを対象に解説しています。Flutterでのビルド方法については[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)をご覧ください。
|
||||||
|
|
||||||
|
Sciter dynamic libraryを事前にダウンロードしてください。
|
||||||
|
|
||||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||||
|
|
||||||
モバイル版はFlutterを利用します。デスクトップ版もSciterからFlutterへマイグレーション予定です。
|
|
||||||
|
|
||||||
## ビルド手順
|
## ビルド手順
|
||||||
|
|
||||||
- Rust開発環境とC ++ビルド環境を準備します
|
- Rust開発環境とC++ビルド環境を準備します。
|
||||||
|
|
||||||
- [vcpkg](https://github.com/microsoft/vcpkg), をインストールし、 `VCPKG_ROOT` 環境変数を正しく設定します。
|
- [vcpkg](https://github.com/microsoft/vcpkg)をインストールし、環境変数に`VCPKG_ROOT`を設定します。
|
||||||
|
その後、以下のコマンドを実行します。
|
||||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
|
||||||
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
|
||||||
|
|
||||||
- run `cargo run`
|
|
||||||
|
|
||||||
|
- Windowsの場合: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
||||||
|
- Linux/macOSの場合: vcpkg install libvpx libyuv opus aom
|
||||||
|
|
||||||
|
- `cargo run`を実行します。
|
||||||
|
|
||||||
## [ビルド](https://rustdesk.com/docs/en/dev/build/)
|
## [ビルド](https://rustdesk.com/docs/en/dev/build/)
|
||||||
|
|
||||||
## Linuxでのビルド手順
|
## Linuxでのビルド方法
|
||||||
|
|
||||||
### Ubuntu 18 (Debian 10)
|
### Ubuntu 18 (Debian 10)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
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
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||||
|
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||||
|
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)
|
### Fedora 28 (CentOS 8)
|
||||||
@@ -69,7 +82,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
|
|||||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install vcpkg
|
### vcpkgのインストール
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/microsoft/vcpkg
|
git clone https://github.com/microsoft/vcpkg
|
||||||
@@ -81,7 +94,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
|||||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fix libvpx (For Fedora)
|
### libvpxの修正 (Fedoraのみ)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd vcpkg/buildtrees/libvpx/src
|
cd vcpkg/buildtrees/libvpx/src
|
||||||
@@ -107,9 +120,9 @@ mv libsciter-gtk.so target/debug
|
|||||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dockerでビルドする方法
|
## Dockerでのビルド方法
|
||||||
|
|
||||||
リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。
|
リポジトリをクローンし、Dockerコンテナを構築します:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/rustdesk/rustdesk
|
git clone https://github.com/rustdesk/rustdesk
|
||||||
@@ -117,44 +130,50 @@ cd rustdesk
|
|||||||
docker build -t "rustdesk-builder" .
|
docker build -t "rustdesk-builder" .
|
||||||
```
|
```
|
||||||
|
|
||||||
その後、アプリケーションをビルドする必要があるたびに、以下のコマンドを実行します。
|
以下のコマンドを実行します:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||||
```
|
```
|
||||||
|
このコマンドはRustDeskをビルドする度に実行する必要があります。
|
||||||
|
|
||||||
なお、最初のビルドでは、依存関係がキャッシュされるまで時間がかかることがありますが、その後のビルドではより速くなります。さらに、ビルドコマンドに別の引数を指定する必要がある場合は、コマンドの最後にある `<OPTIONAL-ARGS>` の位置で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に
|
初回ビルドは時間がかかるかもしれませんが、2回目以降は依存関係がキャッシュされるため、ビルドにかかる時間が短くなります。
|
||||||
`--release` を実行します。できあがった実行ファイルは、システムのターゲット・フォルダに格納され、次のコマンドで実行できます。
|
ビルドコマンドに追加の引数を指定する必要がある場合は、コマンドの最後(`<OPTIONAL-ARGS>`の位置)で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に `--release` を追記し実行します。ビルドされた実行ファイルはあなたのシステムのターゲットフォルダに保存され、下記のコマンドで実行することができます。
|
||||||
|
|
||||||
|
デバッグビルドを起動する場合:
|
||||||
```sh
|
```sh
|
||||||
target/debug/rustdesk
|
target/debug/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
あるいは、リリース用の実行ファイルを実行している場合:
|
リリースビルドを起動する場合:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
target/release/rustdesk
|
target/release/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。
|
コマンドをRustDeskリポジトリのルートから実行していることを確認してください。また、`install` や `run` などの他のcargoサブコマンドは、ホストではなくコンテナ内でプログラムをインストール、実行するため、現在の方法ではサポートされていません。
|
||||||
|
|
||||||
## ファイル構造
|
## ファイル構造
|
||||||
|
|
||||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数
|
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、設定、tcp/udpラッパー、protobuf、ファイル転送に利用されるfs関数やその他のユーティリティ関数
|
||||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
|
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
|
||||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウスコントロール
|
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウス操作
|
||||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS向けのファイルのコピーと貼り付けの実装
|
||||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: オーディオ/クリップボード/入力/ビデオサービス、ネットワーク接続
|
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 廃止された Sciter UI (非推奨)
|
||||||
|
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**:
|
||||||
|
オーディオ/クリップボード/入力/ビデオ サービスとネットワーク接続
|
||||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
|
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始
|
||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)と通信し、リモートの直接接続(TCPホールパンチング)や中継接続を担う。
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
|
||||||
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
|
||||||
|
|
||||||
## スナップショット
|
## スクリーンショット
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import android.widget.EditText
|
|||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import android.view.ViewGroup.LayoutParams
|
import android.view.ViewGroup.LayoutParams
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
|
import android.view.KeyEvent as KeyEventAndroid
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.media.AudioManager
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo
|
import android.accessibilityservice.AccessibilityServiceInfo
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||||
@@ -75,6 +77,8 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
private var fakeEditTextForTextStateCalculation: EditText? = null
|
private var fakeEditTextForTextStateCalculation: EditText? = null
|
||||||
|
|
||||||
|
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
fun onMouseInput(mask: Int, _x: Int, _y: Int) {
|
||||||
val x = max(0, _x)
|
val x = max(0, _x)
|
||||||
@@ -294,6 +298,18 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit")
|
||||||
|
|
||||||
|
var ke: KeyEventAndroid? = null
|
||||||
|
if (Build.VERSION.SDK_INT < 33 || textToCommit == null) {
|
||||||
|
ke = KeyEventConverter.toAndroidKeyEvent(keyEvent)
|
||||||
|
}
|
||||||
|
ke?.let { event ->
|
||||||
|
if (tryHandleVolumeKeyEvent(event)) {
|
||||||
|
return
|
||||||
|
} else if (tryHandlePowerKeyEvent(event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 33) {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
getInputMethod()?.let { inputMethod ->
|
getInputMethod()?.let { inputMethod ->
|
||||||
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
inputMethod.getCurrentInputConnection()?.let { inputConnection ->
|
||||||
@@ -302,7 +318,7 @@ class InputService : AccessibilityService() {
|
|||||||
inputConnection.commitText(text, 1, null)
|
inputConnection.commitText(text, 1, null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
KeyEventConverter.toAndroidKeyEvent(keyEvent).let { event ->
|
ke?.let { event ->
|
||||||
inputConnection.sendKeyEvent(event)
|
inputConnection.sendKeyEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +327,7 @@ class InputService : AccessibilityService() {
|
|||||||
} else {
|
} else {
|
||||||
val handler = Handler(Looper.getMainLooper())
|
val handler = Handler(Looper.getMainLooper())
|
||||||
handler.post {
|
handler.post {
|
||||||
KeyEventConverter.toAndroidKeyEvent(keyEvent)?.let { event ->
|
ke?.let { event ->
|
||||||
val possibleNodes = possibleAccessibiltyNodes()
|
val possibleNodes = possibleAccessibiltyNodes()
|
||||||
Log.d(logTag, "possibleNodes:$possibleNodes")
|
Log.d(logTag, "possibleNodes:$possibleNodes")
|
||||||
for (item in possibleNodes) {
|
for (item in possibleNodes) {
|
||||||
@@ -325,6 +341,43 @@ class InputService : AccessibilityService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tryHandleVolumeKeyEvent(event: KeyEventAndroid): Boolean {
|
||||||
|
when (event.keyCode) {
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_UP -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.raiseVolume(null, true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_DOWN -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.lowerVolume(null, true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEventAndroid.KEYCODE_VOLUME_MUTE -> {
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
|
volumeController.toggleMute(true, AudioManager.STREAM_SYSTEM)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryHandlePowerKeyEvent(event: KeyEventAndroid): Boolean {
|
||||||
|
if (event.keyCode == KeyEventAndroid.KEYCODE_POWER) {
|
||||||
|
// Perform power dialog action when action is up
|
||||||
|
if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||||
|
performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
|
private fun insertAccessibilityNode(list: LinkedList<AccessibilityNodeInfo>, node: AccessibilityNodeInfo) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
return
|
return
|
||||||
@@ -422,7 +475,7 @@ class InputService : AccessibilityService() {
|
|||||||
return linkedList
|
return linkedList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trySendKeyEvent(event: android.view.KeyEvent, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean {
|
||||||
node.refresh()
|
node.refresh()
|
||||||
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
this.fakeEditTextForTextStateCalculation?.setSelection(0,0)
|
||||||
this.fakeEditTextForTextStateCalculation?.setText(null)
|
this.fakeEditTextForTextStateCalculation?.setText(null)
|
||||||
@@ -487,10 +540,10 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
it.layout(rect.left, rect.top, rect.right, rect.bottom)
|
||||||
it.onPreDraw()
|
it.onPreDraw()
|
||||||
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
|
if (event.action == KeyEventAndroid.ACTION_DOWN) {
|
||||||
val succ = it.onKeyDown(event.getKeyCode(), event)
|
val succ = it.onKeyDown(event.getKeyCode(), event)
|
||||||
Log.d(logTag, "onKeyDown $succ")
|
Log.d(logTag, "onKeyDown $succ")
|
||||||
} else if (event.action == android.view.KeyEvent.ACTION_UP) {
|
} else if (event.action == KeyEventAndroid.ACTION_UP) {
|
||||||
val success = it.onKeyUp(event.getKeyCode(), event)
|
val success = it.onKeyUp(event.getKeyCode(), event)
|
||||||
Log.d(logTag, "keyup $success")
|
Log.d(logTag, "keyup $success")
|
||||||
} else {}
|
} else {}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ object KeyEventConverter {
|
|||||||
action = KeyEvent.ACTION_UP
|
action = KeyEvent.ACTION_UP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: The last parameter is the repeat count, not modifiers ?
|
||||||
|
// https://developer.android.com/reference/android/view/KeyEvent#KeyEvent(long,%20long,%20int,%20int,%20int)
|
||||||
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
return KeyEvent(0, 0, action, chrValue, 0, modifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +114,10 @@ object KeyEventConverter {
|
|||||||
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL
|
||||||
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR
|
||||||
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
ControlKey.Pause -> KeyEvent.KEYCODE_BREAK
|
||||||
|
ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE
|
||||||
|
ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP
|
||||||
|
ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN
|
||||||
|
ControlKey.Power -> KeyEvent.KEYCODE_POWER
|
||||||
else -> 0 // Default to unknown.
|
else -> 0 // Default to unknown.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
// Inspired by https://github.com/yosemiteyss/flutter_volume_controller/blob/main/android/src/main/kotlin/com/yosemiteyss/flutter_volume_controller/VolumeController.kt
|
||||||
|
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
class VolumeController(private val audioManager: AudioManager) {
|
||||||
|
private val logTag = "volume controller"
|
||||||
|
|
||||||
|
fun getVolume(streamType: Int): Double {
|
||||||
|
val current = audioManager.getStreamVolume(streamType)
|
||||||
|
val max = audioManager.getStreamMaxVolume(streamType)
|
||||||
|
return current.toDouble() / max
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVolume(volume: Double, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
val max = audioManager.getStreamMaxVolume(streamType)
|
||||||
|
audioManager.setStreamVolume(
|
||||||
|
streamType,
|
||||||
|
(max * volume).toInt(),
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun raiseVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (step == null) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
AudioManager.ADJUST_RAISE,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val target = getVolume(streamType) + step
|
||||||
|
setVolume(target, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lowerVolume(step: Double?, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (step == null) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
AudioManager.ADJUST_LOWER,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val target = getVolume(streamType) - step
|
||||||
|
setVolume(target, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMute(streamType: Int): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
audioManager.isStreamMute(streamType)
|
||||||
|
} else {
|
||||||
|
audioManager.getStreamVolume(streamType) == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMute(isMuted: Boolean, showSystemUI: Boolean, streamType: Int) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
audioManager.adjustStreamVolume(
|
||||||
|
streamType,
|
||||||
|
if (isMuted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
|
||||||
|
if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
audioManager.setStreamMute(streamType, isMuted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleMute(showSystemUI: Boolean, streamType: Int) {
|
||||||
|
val isMuted = getMute(streamType)
|
||||||
|
setMute(!isMuted, showSystemUI, streamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
565
flutter/build_fdroid.sh
Executable file
565
flutter/build_fdroid.sh
Executable file
@@ -0,0 +1,565 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
#
|
||||||
|
# Script to build F-Droid release of RustDesk
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024, The RustDesk Authors
|
||||||
|
# 2024, Vasyl Gello <vasek.gello@gmail.com>
|
||||||
|
#
|
||||||
|
|
||||||
|
# The script is invoked by F-Droid builder system ste-by-step.
|
||||||
|
#
|
||||||
|
# It accepts the following arguments:
|
||||||
|
#
|
||||||
|
# - versionName from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
|
||||||
|
# - versionCode from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt
|
||||||
|
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
|
||||||
|
# - The build step to execute:
|
||||||
|
#
|
||||||
|
# + sudo-deps: as root, install needed Debian packages into builder VM
|
||||||
|
# + prebuild: patch sources and do other stuff before the build
|
||||||
|
# + build: perform actual build of APK file
|
||||||
|
#
|
||||||
|
|
||||||
|
# Parse command-line arguments
|
||||||
|
|
||||||
|
VERNAME="${1}"
|
||||||
|
VERCODE="${2}"
|
||||||
|
ANDROID_ABI="${3}"
|
||||||
|
BUILDSTEP="${4}"
|
||||||
|
|
||||||
|
if [ -z "${VERNAME}" ] || [ -z "${VERCODE}" ] || [ -z "${ANDROID_ABI}" ] ||
|
||||||
|
[ -z "${BUILDSTEP}" ]; then
|
||||||
|
echo "ERROR: Command-line arguments are all required to be non-empty!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set various architecture-specific identifiers
|
||||||
|
|
||||||
|
case "${ANDROID_ABI}" in
|
||||||
|
arm64-v8a)
|
||||||
|
FLUTTER_TARGET=android-arm64
|
||||||
|
NDK_TARGET=aarch64-linux-android
|
||||||
|
RUST_TARGET=aarch64-linux-android
|
||||||
|
# RUSTDESK_FEATURES='flutter,hwcodec'
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
armeabi-v7a)
|
||||||
|
FLUTTER_TARGET=android-arm
|
||||||
|
NDK_TARGET=arm-linux-androideabi
|
||||||
|
RUST_TARGET=armv7-linux-androideabi
|
||||||
|
# RUSTDESK_FEATURES='flutter,hwcodec'
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
FLUTTER_TARGET=android-x64
|
||||||
|
NDK_TARGET=x86_64-linux-android
|
||||||
|
RUST_TARGET=x86_64-linux-android
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
x86)
|
||||||
|
FLUTTER_TARGET=android-x86
|
||||||
|
NDK_TARGET=i686-linux-android
|
||||||
|
RUST_TARGET=i686-linux-android
|
||||||
|
RUSTDESK_FEATURES='flutter'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown Android ABI '${ANDROID_ABI}'!" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check ANDROID_SDK_ROOT and sdkmanager present on PATH
|
||||||
|
|
||||||
|
if [ ! -d "${ANDROID_SDK_ROOT}" ] || ! command -v sdkmanager 1>/dev/null; then
|
||||||
|
echo "ERROR: Can not find Android SDK!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export necessary variables
|
||||||
|
|
||||||
|
export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools"
|
||||||
|
|
||||||
|
export VCPKG_ROOT="${HOME}/vcpkg"
|
||||||
|
|
||||||
|
# Now act depending on build step
|
||||||
|
|
||||||
|
# NOTE: F-Droid maintainers require explicit declaration of dependencies
|
||||||
|
# as root via `Builds.sudo` F-Droid metadata directive:
|
||||||
|
# https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695
|
||||||
|
|
||||||
|
case "${BUILDSTEP}" in
|
||||||
|
prebuild)
|
||||||
|
# prebuild: patch sources and do other stuff before the build
|
||||||
|
|
||||||
|
#
|
||||||
|
# Extract required versions for NDK, Rust, Flutter from
|
||||||
|
# '.github/workflows/flutter-build.yml'
|
||||||
|
#
|
||||||
|
|
||||||
|
CARGO_NDK_VERSION="$(yq -r \
|
||||||
|
.env.CARGO_NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.ANDROID_FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_RUST_BRIDGE_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
NDK_VERSION="$(yq -r \
|
||||||
|
.env.NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
RUST_VERSION="$(yq -r \
|
||||||
|
.env.RUST_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
VCPKG_COMMIT_ID="$(yq -r \
|
||||||
|
.env.VCPKG_COMMIT_ID \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] ||
|
||||||
|
[ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] ||
|
||||||
|
[ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] ||
|
||||||
|
[ -z "${VCPKG_COMMIT_ID}" ]; then
|
||||||
|
echo "ERROR: Can not identify all required versions!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Map NDK version to revision
|
||||||
|
|
||||||
|
NDK_VERSION="$(wget \
|
||||||
|
-qO- \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
'https://api.github.com/repos/android/ndk/releases' |
|
||||||
|
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||||
|
|
||||||
|
if [ -z "${NDK_VERSION}" ]; then
|
||||||
|
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Install the components
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install Android NDK
|
||||||
|
|
||||||
|
if [ ! -d "${ANDROID_NDK_ROOT}" ]; then
|
||||||
|
sdkmanager --install "ndk;${NDK_VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Flutter
|
||||||
|
|
||||||
|
if [ ! -f "${HOME}/flutter/bin/flutter" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone https://github.com/flutter/flutter
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
git reset --hard "${FLUTTER_VERSION}"
|
||||||
|
|
||||||
|
flutter config --no-analytics
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Rust
|
||||||
|
|
||||||
|
if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone --depth 1 https://github.com/rust-lang/rustup
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd "${HOME}/rustup"
|
||||||
|
bash rustup-init.sh -y \
|
||||||
|
--target "${RUST_TARGET}" \
|
||||||
|
--default-toolchain "${RUST_VERSION}"
|
||||||
|
popd
|
||||||
|
|
||||||
|
if ! command -v cargo 1>/dev/null 2>&1; then
|
||||||
|
. "${HOME}/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install cargo-ndk
|
||||||
|
|
||||||
|
cargo install \
|
||||||
|
cargo-ndk \
|
||||||
|
--version "${CARGO_NDK_VERSION}"
|
||||||
|
|
||||||
|
# Install rust bridge generator
|
||||||
|
|
||||||
|
cargo install cargo-expand
|
||||||
|
cargo install flutter_rust_bridge_codegen \
|
||||||
|
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
|
||||||
|
--features "uuid"
|
||||||
|
|
||||||
|
# Populate native vcpkg dependencies
|
||||||
|
|
||||||
|
if [ ! -d "${VCPKG_ROOT}" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
https://github.com/Microsoft/vcpkg.git
|
||||||
|
git clone \
|
||||||
|
https://github.com/Microsoft/vcpkg-tool.git
|
||||||
|
|
||||||
|
pushd vcpkg-tool
|
||||||
|
|
||||||
|
mkdir build
|
||||||
|
|
||||||
|
pushd build
|
||||||
|
|
||||||
|
cmake \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-G 'Ninja' \
|
||||||
|
-DVCPKG_DEVELOPMENT_WARNINGS=OFF \
|
||||||
|
..
|
||||||
|
|
||||||
|
cmake --build .
|
||||||
|
|
||||||
|
popd # build
|
||||||
|
|
||||||
|
popd # vcpkg-tool
|
||||||
|
|
||||||
|
pushd vcpkg
|
||||||
|
|
||||||
|
git reset --hard "${VCPKG_COMMIT_ID}"
|
||||||
|
|
||||||
|
cp -a ../vcpkg-tool/build/vcpkg vcpkg
|
||||||
|
|
||||||
|
# disable telemetry
|
||||||
|
|
||||||
|
touch "vcpkg.disable-metrics"
|
||||||
|
|
||||||
|
popd # vcpkg
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install depot-tools for x86
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
if [ ! -d "${HOME}/depot_tools" ]; then
|
||||||
|
pushd "${HOME}"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
--depth 1 \
|
||||||
|
https://chromium.googlesource.com/chromium/tools/depot_tools.git
|
||||||
|
|
||||||
|
popd # ${HOME}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Patch the RustDesk sources
|
||||||
|
|
||||||
|
git apply res/fdroid/patches/*.patch
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/gms/d' \
|
||||||
|
flutter/android/build.gradle \
|
||||||
|
flutter/android/app/build.gradle
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/firebase_analytics/d' \
|
||||||
|
flutter/pubspec.yaml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/ firebase/,/ version/d' \
|
||||||
|
flutter/pubspec.lock
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/firebase/Id' \
|
||||||
|
flutter/lib/main.dart
|
||||||
|
|
||||||
|
if [ "${FLUTTER_VERSION}" = "3.13.9" ]; then
|
||||||
|
# Fix for android 3.13.9
|
||||||
|
# https://github.com/rustdesk/rustdesk/blob/285e974d1a52c891d5fcc28e963d724e085558bc/.github/workflows/flutter-build.yml#L862
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e 's/uni_links_desktop/#uni_links_desktop/g' \
|
||||||
|
flutter/pubspec.yaml
|
||||||
|
|
||||||
|
set --
|
||||||
|
|
||||||
|
while read -r _1; do
|
||||||
|
set -- "$@" "${_1}"
|
||||||
|
done 0<<.a
|
||||||
|
$(find flutter/lib/ -type f -name "*dart*")
|
||||||
|
.a
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
set --
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient
|
||||||
|
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
# build: perform actual build of APK file
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#
|
||||||
|
# Extract required versions for NDK, Rust, Flutter from
|
||||||
|
# '.github/workflows/flutter-build.yml'
|
||||||
|
#
|
||||||
|
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.ANDROID_FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
if [ -z "${FLUTTER_VERSION}" ]; then
|
||||||
|
FLUTTER_VERSION="$(yq -r \
|
||||||
|
.env.FLUTTER_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
NDK_VERSION="$(yq -r \
|
||||||
|
.env.NDK_VERSION \
|
||||||
|
.github/workflows/flutter-build.yml)"
|
||||||
|
|
||||||
|
# Map NDK version to revision
|
||||||
|
|
||||||
|
NDK_VERSION="$(wget \
|
||||||
|
-qO- \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
'https://api.github.com/repos/android/ndk/releases' |
|
||||||
|
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
|
||||||
|
|
||||||
|
if [ -z "${NDK_VERSION}" ]; then
|
||||||
|
echo "ERROR: Can not map Android NDK codename to revision!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||||
|
|
||||||
|
if ! command -v cargo 1>/dev/null 2>&1; then
|
||||||
|
. "${HOME}/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download Flutter dependencies
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
flutter packages pub get
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
# Generate FFI bindings
|
||||||
|
|
||||||
|
flutter_rust_bridge_codegen \
|
||||||
|
--rust-input ./src/flutter_ffi.rs \
|
||||||
|
--dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
|
# Build host android deps
|
||||||
|
|
||||||
|
bash flutter/build_android_deps.sh "${ANDROID_ABI}"
|
||||||
|
|
||||||
|
# Build rustdesk lib
|
||||||
|
|
||||||
|
cargo ndk \
|
||||||
|
--platform 21 \
|
||||||
|
--target "${RUST_TARGET}" \
|
||||||
|
--bindgen \
|
||||||
|
build \
|
||||||
|
--release \
|
||||||
|
--features "${RUSTDESK_FEATURES}"
|
||||||
|
|
||||||
|
mkdir -p "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"
|
||||||
|
|
||||||
|
cp "target/${RUST_TARGET}/release/liblibrustdesk.so" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so"
|
||||||
|
|
||||||
|
cp "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/${NDK_TARGET}/libc++_shared.so" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/"
|
||||||
|
|
||||||
|
"${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" \
|
||||||
|
"flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"/*
|
||||||
|
|
||||||
|
# Build flutter-jit-release for x86
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
pushd flutter-sdk
|
||||||
|
|
||||||
|
echo "## Sync flutter engine sources"
|
||||||
|
echo "### We need fakeroot because chromium base image is unpacked with weird uid/gid ownership"
|
||||||
|
|
||||||
|
sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" .gclient
|
||||||
|
|
||||||
|
export FAKEROOTDONTTRYCHOWN=1
|
||||||
|
|
||||||
|
fakeroot gclient sync
|
||||||
|
|
||||||
|
unset FAKEROOTDONTTRYCHOWN
|
||||||
|
|
||||||
|
pushd src
|
||||||
|
|
||||||
|
echo "## Patch away Google Play dependencies"
|
||||||
|
|
||||||
|
rm \
|
||||||
|
flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java \
|
||||||
|
flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/PlayStore/d' \
|
||||||
|
flutter/tools/android_lint/project.xml \
|
||||||
|
flutter/shell/platform/android/BUILD.gn
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-i \
|
||||||
|
-e '/com.google.android.play/d' \
|
||||||
|
flutter/tools/androidx/files.json
|
||||||
|
|
||||||
|
echo "## Configure android engine build"
|
||||||
|
|
||||||
|
flutter/tools/gn \
|
||||||
|
--android --android-cpu x86 --runtime-mode=jit_release \
|
||||||
|
--no-goma --no-enable-unittests
|
||||||
|
|
||||||
|
echo "## Perform android engine build"
|
||||||
|
|
||||||
|
ninja -C out/android_jit_release_x86
|
||||||
|
|
||||||
|
echo "## Configure host engine build"
|
||||||
|
|
||||||
|
flutter/tools/gn \
|
||||||
|
--android-cpu x86 --runtime-mode=jit_release \
|
||||||
|
--no-goma --no-enable-unittests
|
||||||
|
|
||||||
|
echo "## Perform android engine build"
|
||||||
|
|
||||||
|
ninja -C out/host_jit_release_x86
|
||||||
|
|
||||||
|
echo "## Rename host engine"
|
||||||
|
|
||||||
|
mv out/host_jit_release_x86 out/host_jit_release
|
||||||
|
|
||||||
|
echo "## Mimic jit_release engine to debug to use with flutter build apk"
|
||||||
|
|
||||||
|
pushd out/android_jit_release_x86
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
flutter_embedding_jit_release.maven-metadata.xml \
|
||||||
|
1>flutter_embedding_debug.maven-metadata.xml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
flutter_embedding_jit_release.pom \
|
||||||
|
1>flutter_embedding_debug.pom
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
x86_jit_release.maven-metadata.xml \
|
||||||
|
1>x86_debug.maven-metadata.xml
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e 's/jit_release/debug/' \
|
||||||
|
x86_jit_release.pom \
|
||||||
|
1>x86_debug.pom
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
flutter_embedding_jit_release-sources.jar \
|
||||||
|
flutter_embedding_debug-sources.jar
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
flutter_embedding_jit_release.jar \
|
||||||
|
flutter_embedding_debug.jar
|
||||||
|
|
||||||
|
cp -a \
|
||||||
|
x86_jit_release.jar \
|
||||||
|
x86_debug.jar
|
||||||
|
|
||||||
|
popd # out/android_jit_release_x86
|
||||||
|
|
||||||
|
popd # src
|
||||||
|
|
||||||
|
popd # flutter-sdk
|
||||||
|
|
||||||
|
echo "# Clean up intermediate engine files and show free space"
|
||||||
|
|
||||||
|
rm -rf \
|
||||||
|
flutter-sdk/src/out/android_jit_release_x86/obj \
|
||||||
|
flutter-sdk/src/out/host_jit_release/obj
|
||||||
|
|
||||||
|
mv flutter-sdk/src/out flutter-out
|
||||||
|
|
||||||
|
rm -rf flutter-sdk
|
||||||
|
|
||||||
|
mkdir -p flutter-sdk/src/
|
||||||
|
|
||||||
|
mv flutter-out flutter-sdk/src/out
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the apk
|
||||||
|
|
||||||
|
pushd flutter
|
||||||
|
|
||||||
|
if [ "${ANDROID_ABI}" = "x86" ]; then
|
||||||
|
flutter build apk \
|
||||||
|
--local-engine-src-path="$(readlink -mf "../flutter-sdk/src")" \
|
||||||
|
--local-engine=android_jit_release_x86 \
|
||||||
|
--debug \
|
||||||
|
--build-number="${VERCODE}" \
|
||||||
|
--build-name="${VERNAME}" \
|
||||||
|
--target-platform "${FLUTTER_TARGET}"
|
||||||
|
else
|
||||||
|
flutter build apk \
|
||||||
|
--release \
|
||||||
|
--build-number="${VERCODE}" \
|
||||||
|
--build-name="${VERNAME}" \
|
||||||
|
--target-platform "${FLUTTER_TARGET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
popd # flutter
|
||||||
|
|
||||||
|
rm -rf flutter-sdk
|
||||||
|
|
||||||
|
# Special step for fdroiddata CI builds to remove .gitconfig
|
||||||
|
|
||||||
|
rm -f /home/vagrant/.gitconfig
|
||||||
|
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown build step '${BUILDSTEP}'!" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Report success
|
||||||
|
|
||||||
|
echo "All done!"
|
||||||
@@ -928,13 +928,9 @@ makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
|||||||
position: draggablePositions.mobileActions,
|
position: draggablePositions.mobileActions,
|
||||||
width: overlayW,
|
width: overlayW,
|
||||||
height: overlayH,
|
height: overlayH,
|
||||||
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
onBackPressed: session.inputModel.onMobileBack,
|
||||||
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
onHomePressed: session.inputModel.onMobileHome,
|
||||||
onRecentPressed: () async {
|
onRecentPressed: session.inputModel.onMobileApps,
|
||||||
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
|
||||||
},
|
|
||||||
onHidePressed: onHide,
|
onHidePressed: onHide,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1066,7 +1062,7 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
bool hasOk = false;
|
bool hasOk = false;
|
||||||
submit() {
|
submit() {
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
// https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||||
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
|
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
|
||||||
closeConnection();
|
closeConnection();
|
||||||
}
|
}
|
||||||
@@ -1100,21 +1096,33 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (reconnect != null &&
|
if (reconnect != null && title == "Connection Error") {
|
||||||
title == "Connection Error" &&
|
|
||||||
reconnectTimeout != null) {
|
|
||||||
// `enabled` is used to disable the dialog button once the button is clicked.
|
// `enabled` is used to disable the dialog button once the button is clicked.
|
||||||
final enabled = true.obs;
|
final enabled = true.obs;
|
||||||
final button = Obx(() => _ReconnectCountDownButton(
|
final button = reconnectTimeout != null
|
||||||
second: reconnectTimeout,
|
? Obx(() => _ReconnectCountDownButton(
|
||||||
onPressed: enabled.isTrue
|
second: reconnectTimeout,
|
||||||
? () {
|
onPressed: enabled.isTrue
|
||||||
// Disable the button
|
? () {
|
||||||
enabled.value = false;
|
// Disable the button
|
||||||
reconnect(dialogManager, sessionId, false);
|
enabled.value = false;
|
||||||
}
|
reconnect(dialogManager, sessionId, false);
|
||||||
: null,
|
}
|
||||||
));
|
: null,
|
||||||
|
))
|
||||||
|
: Obx(
|
||||||
|
() => dialogButton(
|
||||||
|
'Reconnect',
|
||||||
|
isOutline: true,
|
||||||
|
onPressed: enabled.isTrue
|
||||||
|
? () {
|
||||||
|
// Disable the button
|
||||||
|
enabled.value = false;
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
buttons.insert(0, button);
|
buttons.insert(0, button);
|
||||||
}
|
}
|
||||||
if (link.isNotEmpty) {
|
if (link.isNotEmpty) {
|
||||||
@@ -1432,7 +1440,7 @@ Future<void> initGlobalFFI() async {
|
|||||||
_globalFFI = FFI(null);
|
_globalFFI = FFI(null);
|
||||||
debugPrint("_globalFFI init end");
|
debugPrint("_globalFFI init end");
|
||||||
// after `put`, can also be globally found by Get.find<FFI>();
|
// after `put`, can also be globally found by Get.find<FFI>();
|
||||||
Get.put(_globalFFI, permanent: true);
|
Get.put<FFI>(_globalFFI, permanent: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
String translate(String name) {
|
String translate(String name) {
|
||||||
@@ -2719,20 +2727,26 @@ Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
typedef WhetherUseRemoteBlock = Future<bool> Function();
|
||||||
Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
|
Widget buildRemoteBlock(
|
||||||
var block = false.obs;
|
{required Widget child,
|
||||||
|
required RxBool block,
|
||||||
|
required bool mask,
|
||||||
|
WhetherUseRemoteBlock? use}) {
|
||||||
return Obx(() => MouseRegion(
|
return Obx(() => MouseRegion(
|
||||||
onEnter: (_) async {
|
onEnter: (_) async {
|
||||||
await shouldBeBlocked(block, use);
|
await shouldBeBlocked(block, use);
|
||||||
},
|
},
|
||||||
onExit: (event) => block.value = false,
|
onExit: (event) => block.value = false,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
child,
|
// scope block tab
|
||||||
Offstage(
|
FocusScope(child: child, canRequestFocus: !block.value),
|
||||||
offstage: !block.value,
|
// mask block click, cm not block click and still use check_click_time to avoid block local click
|
||||||
child: Container(
|
if (mask)
|
||||||
color: Colors.black.withOpacity(0.5),
|
Offstage(
|
||||||
)),
|
offstage: !block.value,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
)),
|
||||||
]),
|
]),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -3084,9 +3098,16 @@ Future<bool> setServerConfig(
|
|||||||
List<RxString>? errMsgs,
|
List<RxString>? errMsgs,
|
||||||
ServerConfig config,
|
ServerConfig config,
|
||||||
) async {
|
) async {
|
||||||
config.idServer = config.idServer.trim();
|
String removeEndSlash(String input) {
|
||||||
config.relayServer = config.relayServer.trim();
|
if (input.endsWith('/')) {
|
||||||
config.apiServer = config.apiServer.trim();
|
return input.substring(0, input.length - 1);
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.idServer = removeEndSlash(config.idServer.trim());
|
||||||
|
config.relayServer = removeEndSlash(config.relayServer.trim());
|
||||||
|
config.apiServer = removeEndSlash(config.apiServer.trim());
|
||||||
config.key = config.key.trim();
|
config.key = config.key.trim();
|
||||||
if (controllers != null) {
|
if (controllers != null) {
|
||||||
controllers[0].text = config.idServer;
|
controllers[0].text = config.idServer;
|
||||||
@@ -3305,6 +3326,11 @@ Widget buildPresetPasswordWarning() {
|
|||||||
return Text(
|
return Text(
|
||||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||||
} else if (snapshot.hasData && snapshot.data == true) {
|
} else if (snapshot.hasData && snapshot.data == true) {
|
||||||
|
if (bind.mainGetBuildinOption(
|
||||||
|
key: kOptionRemovePresetPasswordWarning) !=
|
||||||
|
'N') {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.yellow,
|
color: Colors.yellow,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -3406,6 +3432,12 @@ get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
|||||||
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
||||||
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
||||||
|
|
||||||
|
bool whitelistNotEmpty() {
|
||||||
|
// https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist
|
||||||
|
final v = bind.mainGetOptionSync(key: kOptionWhitelist);
|
||||||
|
return v != '' && v != ',';
|
||||||
|
}
|
||||||
|
|
||||||
// `setMovable()` is only supported on macOS.
|
// `setMovable()` is only supported on macOS.
|
||||||
//
|
//
|
||||||
// On macOS, the window can be dragged by the tab bar by default.
|
// On macOS, the window can be dragged by the tab bar by default.
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ class PrivacyModeState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString state = ''.obs;
|
final RxString state = ''.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxString>(state, tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = '';
|
Get.find<RxString>(tag: key).value = '';
|
||||||
}
|
}
|
||||||
@@ -33,9 +33,9 @@ class BlockInputState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,8 @@ class BlockInputState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,9 +56,9 @@ class CurrentDisplayState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = RxInt(0);
|
final RxInt state = RxInt(0);
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 0;
|
Get.find<RxInt>(tag: key).value = 0;
|
||||||
}
|
}
|
||||||
@@ -66,8 +66,8 @@ class CurrentDisplayState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,16 +105,16 @@ class ConnectionTypeState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<ConnectionType>(tag: key)) {
|
||||||
final ConnectionType collectionType = ConnectionType();
|
final ConnectionType collectionType = ConnectionType();
|
||||||
Get.put(collectionType, tag: key);
|
Get.put<ConnectionType>(collectionType, tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<ConnectionType>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<ConnectionType>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +127,9 @@ class FingerprintState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString state = ''.obs;
|
final RxString state = ''.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxString>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = '';
|
Get.find<RxString>(tag: key).value = '';
|
||||||
}
|
}
|
||||||
@@ -137,8 +137,8 @@ class FingerprintState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,9 +150,9 @@ class ShowRemoteCursorState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -160,8 +160,8 @@ class ShowRemoteCursorState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +173,9 @@ class ShowRemoteCursorLockState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -183,8 +183,8 @@ class ShowRemoteCursorLockState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,10 +196,10 @@ class KeyboardEnabledState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
// Server side, default true
|
// Server side, default true
|
||||||
final RxBool state = true.obs;
|
final RxBool state = true.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = true;
|
Get.find<RxBool>(tag: key).value = true;
|
||||||
}
|
}
|
||||||
@@ -207,8 +207,8 @@ class KeyboardEnabledState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,9 +220,9 @@ class RemoteCursorMovedState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxBool>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = false;
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
}
|
}
|
||||||
@@ -230,8 +230,8 @@ class RemoteCursorMovedState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +243,9 @@ class RemoteCountState {
|
|||||||
|
|
||||||
static void init() {
|
static void init() {
|
||||||
final key = tag();
|
final key = tag();
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = 1.obs;
|
final RxInt state = 1.obs;
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 1;
|
Get.find<RxInt>(tag: key).value = 1;
|
||||||
}
|
}
|
||||||
@@ -253,8 +253,8 @@ class RemoteCountState {
|
|||||||
|
|
||||||
static void delete() {
|
static void delete() {
|
||||||
final key = tag();
|
final key = tag();
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +266,9 @@ class PeerBoolOption {
|
|||||||
|
|
||||||
static void init(String id, String opt, bool Function() init_getter) {
|
static void init(String id, String opt, bool Function() init_getter) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxBool>(tag: key)) {
|
||||||
final RxBool value = RxBool(init_getter());
|
final RxBool value = RxBool(init_getter());
|
||||||
Get.put(value, tag: key);
|
Get.put<RxBool>(value, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxBool>(tag: key).value = init_getter();
|
Get.find<RxBool>(tag: key).value = init_getter();
|
||||||
}
|
}
|
||||||
@@ -276,8 +276,8 @@ class PeerBoolOption {
|
|||||||
|
|
||||||
static void delete(String id, String opt) {
|
static void delete(String id, String opt) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxBool>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxBool>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,9 +290,9 @@ class PeerStringOption {
|
|||||||
|
|
||||||
static void init(String id, String opt, String Function() init_getter) {
|
static void init(String id, String opt, String Function() init_getter) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxString>(tag: key)) {
|
||||||
final RxString value = RxString(init_getter());
|
final RxString value = RxString(init_getter());
|
||||||
Get.put(value, tag: key);
|
Get.put<RxString>(value, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxString>(tag: key).value = init_getter();
|
Get.find<RxString>(tag: key).value = init_getter();
|
||||||
}
|
}
|
||||||
@@ -300,8 +300,8 @@ class PeerStringOption {
|
|||||||
|
|
||||||
static void delete(String id, String opt) {
|
static void delete(String id, String opt) {
|
||||||
final key = tag(id, opt);
|
final key = tag(id, opt);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxString>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxString>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,9 +314,9 @@ class UnreadChatCountState {
|
|||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (!Get.isRegistered(tag: key)) {
|
if (!Get.isRegistered<RxInt>(tag: key)) {
|
||||||
final RxInt state = RxInt(0);
|
final RxInt state = RxInt(0);
|
||||||
Get.put(state, tag: key);
|
Get.put<RxInt>(state, tag: key);
|
||||||
} else {
|
} else {
|
||||||
Get.find<RxInt>(tag: key).value = 0;
|
Get.find<RxInt>(tag: key).value = 0;
|
||||||
}
|
}
|
||||||
@@ -324,8 +324,8 @@ class UnreadChatCountState {
|
|||||||
|
|
||||||
static void delete(String id) {
|
static void delete(String id) {
|
||||||
final key = tag(id);
|
final key = tag(id);
|
||||||
if (Get.isRegistered(tag: key)) {
|
if (Get.isRegistered<RxInt>(tag: key)) {
|
||||||
Get.delete(tag: key);
|
Get.delete<RxInt>(tag: key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,39 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
|
||||||
|
const _kWindowsSystemSound = 'System Sound';
|
||||||
|
|
||||||
typedef AudioINputSetDevice = void Function(String device);
|
typedef AudioINputSetDevice = void Function(String device);
|
||||||
typedef AudioInputBuilder = Widget Function(
|
typedef AudioInputBuilder = Widget Function(
|
||||||
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
||||||
|
|
||||||
class AudioInput extends StatelessWidget {
|
class AudioInput extends StatelessWidget {
|
||||||
final AudioInputBuilder builder;
|
final AudioInputBuilder builder;
|
||||||
|
final bool isCm;
|
||||||
|
final bool isVoiceCall;
|
||||||
|
|
||||||
const AudioInput({Key? key, required this.builder}) : super(key: key);
|
const AudioInput(
|
||||||
|
{Key? key,
|
||||||
|
required this.builder,
|
||||||
|
required this.isCm,
|
||||||
|
required this.isVoiceCall})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
static String getDefault() {
|
static String getDefault() {
|
||||||
if (isWindows) return translate('System Sound');
|
if (isWindows) return translate('System Sound');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String> getValue() async {
|
static Future<String> getAudioInput(bool isCm, bool isVoiceCall) {
|
||||||
String device = await bind.mainGetOption(key: 'audio-input');
|
if (isVoiceCall) {
|
||||||
|
return bind.getVoiceCallInputDevice(isCm: isCm);
|
||||||
|
} else {
|
||||||
|
return bind.mainGetOption(key: 'audio-input');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> getValue(bool isCm, bool isVoiceCall) async {
|
||||||
|
String device = await getAudioInput(isCm, isVoiceCall);
|
||||||
if (device.isNotEmpty) {
|
if (device.isNotEmpty) {
|
||||||
return device;
|
return device;
|
||||||
} else {
|
} else {
|
||||||
@@ -25,31 +42,39 @@ class AudioInput extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setDevice(String device) async {
|
static Future<void> setDevice(
|
||||||
|
String device, bool isCm, bool isVoiceCall) async {
|
||||||
if (device == getDefault()) device = '';
|
if (device == getDefault()) device = '';
|
||||||
await bind.mainSetOption(key: 'audio-input', value: device);
|
if (isVoiceCall) {
|
||||||
|
await bind.setVoiceCallInputDevice(isCm: isCm, device: device);
|
||||||
|
} else {
|
||||||
|
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, Object>> getDevicesInfo() async {
|
static Future<Map<String, Object>> getDevicesInfo(
|
||||||
|
bool isCm, bool isVoiceCall) async {
|
||||||
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
devices.insert(0, translate('System Sound'));
|
devices.insert(0, translate(_kWindowsSystemSound));
|
||||||
}
|
}
|
||||||
String current = await getValue();
|
String current = await getValue(isCm, isVoiceCall);
|
||||||
return {'devices': devices, 'current': current};
|
return {'devices': devices, 'current': current};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return futureBuilder(
|
return futureBuilder(
|
||||||
future: getDevicesInfo(),
|
future: getDevicesInfo(isCm, isVoiceCall),
|
||||||
hasData: (data) {
|
hasData: (data) {
|
||||||
String currentDevice = data['current'];
|
String currentDevice = data['current'];
|
||||||
List<String> devices = data['devices'] as List<String>;
|
List<String> devices = data['devices'] as List<String>;
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
return builder(devices, currentDevice, setDevice);
|
return builder(devices, currentDevice, (devices) {
|
||||||
|
setDevice(devices, isCm, isVoiceCall);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
@@ -218,50 +219,53 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
if (!isOptFixed)dialogButton("Clear", onPressed: () async {
|
if (!isOptFixed)
|
||||||
await bind.mainSetOption(
|
dialogButton("Clear", onPressed: () async {
|
||||||
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
|
||||||
callback?.call();
|
|
||||||
close();
|
|
||||||
}, isOutline: true),
|
|
||||||
if (!isOptFixed) dialogButton(
|
|
||||||
"OK",
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
|
||||||
msg = "";
|
|
||||||
isInProgress = true;
|
|
||||||
});
|
|
||||||
newWhiteListField = controller.text.trim();
|
|
||||||
var newWhiteList = "";
|
|
||||||
if (newWhiteListField.isEmpty) {
|
|
||||||
// pass
|
|
||||||
} else {
|
|
||||||
final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
|
||||||
// test ip
|
|
||||||
final ipMatch = RegExp(
|
|
||||||
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
|
||||||
final ipv6Match = RegExp(
|
|
||||||
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
|
|
||||||
for (final ip in ips) {
|
|
||||||
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
|
|
||||||
msg = "${translate("Invalid IP")} $ip";
|
|
||||||
setState(() {
|
|
||||||
isInProgress = false;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newWhiteList = ips.join(',');
|
|
||||||
}
|
|
||||||
if (newWhiteList.trim().isEmpty) {
|
|
||||||
newWhiteList = defaultOptionWhitelist;
|
|
||||||
}
|
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: kOptionWhitelist, value: newWhiteList);
|
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||||
callback?.call();
|
callback?.call();
|
||||||
close();
|
close();
|
||||||
},
|
}, isOutline: true),
|
||||||
),
|
if (!isOptFixed)
|
||||||
|
dialogButton(
|
||||||
|
"OK",
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
msg = "";
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
newWhiteListField = controller.text.trim();
|
||||||
|
var newWhiteList = "";
|
||||||
|
if (newWhiteListField.isEmpty) {
|
||||||
|
// pass
|
||||||
|
} else {
|
||||||
|
final ips =
|
||||||
|
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
||||||
|
// test ip
|
||||||
|
final ipMatch = RegExp(
|
||||||
|
r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
|
||||||
|
final ipv6Match = RegExp(
|
||||||
|
r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
|
||||||
|
for (final ip in ips) {
|
||||||
|
if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
|
||||||
|
msg = "${translate("Invalid IP")} $ip";
|
||||||
|
setState(() {
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newWhiteList = ips.join(',');
|
||||||
|
}
|
||||||
|
if (newWhiteList.trim().isEmpty) {
|
||||||
|
newWhiteList = defaultOptionWhitelist;
|
||||||
|
}
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionWhitelist, value: newWhiteList);
|
||||||
|
callback?.call();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onCancel: close,
|
onCancel: close,
|
||||||
);
|
);
|
||||||
@@ -1762,6 +1766,66 @@ void renameDialog(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void changeBot({Function()? callback}) async {
|
||||||
|
if (bind.mainHasValidBotSync()) {
|
||||||
|
await bind.mainSetOption(key: "bot", value: "");
|
||||||
|
callback?.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String errorText = '';
|
||||||
|
bool loading = false;
|
||||||
|
final controller = TextEditingController();
|
||||||
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
onVerify() async {
|
||||||
|
final token = controller.text.trim();
|
||||||
|
if (token == "") return;
|
||||||
|
loading = true;
|
||||||
|
errorText = '';
|
||||||
|
setState(() {});
|
||||||
|
final error = await bind.mainVerifyBot(token: token);
|
||||||
|
if (error == "") {
|
||||||
|
callback?.call();
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
errorText = translate(error);
|
||||||
|
loading = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final codeField = TextField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: translate('Token'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Telegram bot")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SelectableText(translate("enable-bot-desc"),
|
||||||
|
style: TextStyle(fontSize: 12))
|
||||||
|
.marginOnly(bottom: 12),
|
||||||
|
Row(children: [Expanded(child: codeField)]),
|
||||||
|
if (errorText != '')
|
||||||
|
Text(errorText, style: TextStyle(color: Colors.red))
|
||||||
|
.marginOnly(top: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
|
loading
|
||||||
|
? CircularProgressIndicator()
|
||||||
|
: dialogButton("OK", onPressed: onVerify),
|
||||||
|
],
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void change2fa({Function()? callback}) async {
|
void change2fa({Function()? callback}) async {
|
||||||
if (bind.mainHasValid2FaSync()) {
|
if (bind.mainHasValid2FaSync()) {
|
||||||
await bind.mainSetOption(key: "2fa", value: "");
|
await bind.mainSetOption(key: "2fa", value: "");
|
||||||
@@ -2124,3 +2188,31 @@ void setSharedAbPasswordDialog(String abName, Peer peer) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
|
||||||
|
VoidCallback onConfirm) {
|
||||||
|
dialogManager.show((setState, close, context) {
|
||||||
|
submit() {
|
||||||
|
close();
|
||||||
|
onConfirm.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(content,
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
textAlign: TextAlign.start),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).marginOnly(bottom: 12),
|
||||||
|
actions: [
|
||||||
|
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||||
|
dialogButton(translate("OK"), onPressed: submit),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
_height = widget.height;
|
_height = widget.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
get position => widget.position;
|
DraggableKeyPosition get position => widget.position;
|
||||||
|
|
||||||
checkKeyboard() {
|
checkKeyboard() {
|
||||||
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||||
@@ -494,13 +494,13 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
|
|
||||||
// save
|
// save
|
||||||
if (!_keyboardVisible && currentVisible) {
|
if (!_keyboardVisible && currentVisible) {
|
||||||
_saveHeight = position.value.dy;
|
_saveHeight = position.pos.dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
position.value = Offset(position.value.dx, _saveHeight);
|
position.update(Offset(position.pos.dx, _saveHeight));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,10 +508,10 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
if (_keyboardVisible && currentVisible) {
|
if (_keyboardVisible && currentVisible) {
|
||||||
final sumHeight = bottomHeight + _height;
|
final sumHeight = bottomHeight + _height;
|
||||||
final contextHeight = MediaQuery.of(context).size.height;
|
final contextHeight = MediaQuery.of(context).size.height;
|
||||||
if (sumHeight + position.value.dy > contextHeight) {
|
if (sumHeight + position.pos.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
position.value = Offset(position.value.dx, y);
|
position.update(Offset(position.pos.dx, y));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,14 +526,14 @@ class IOSDraggableState extends State<IOSDraggable> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
left: position.value.dx,
|
left: position.pos.dx,
|
||||||
top: position.value.dy,
|
top: position.pos.dy,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onPanUpdate: (details) {
|
onPanUpdate: (details) {
|
||||||
setState(() {
|
setState(() {
|
||||||
position.value += details.delta;
|
position.update(position.pos + details.delta);
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(position.value);
|
_chatModel?.setChatWindowPosition(position.pos);
|
||||||
},
|
},
|
||||||
child: Material(
|
child: Material(
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ enum PeerUiType { grid, tile, list }
|
|||||||
|
|
||||||
final peerCardUiType = PeerUiType.grid.obs;
|
final peerCardUiType = PeerUiType.grid.obs;
|
||||||
|
|
||||||
|
bool? hideUsernameOnCard;
|
||||||
|
|
||||||
class _PeerCard extends StatefulWidget {
|
class _PeerCard extends StatefulWidget {
|
||||||
final Peer peer;
|
final Peer peer;
|
||||||
final PeerTabIndex tab;
|
final PeerTabIndex tab;
|
||||||
@@ -130,8 +132,11 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
|
|
||||||
Widget _buildPeerTile(
|
Widget _buildPeerTile(
|
||||||
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
|
||||||
final name =
|
hideUsernameOnCard ??=
|
||||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||||
|
final name = hideUsernameOnCard == true
|
||||||
|
? peer.hostname
|
||||||
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||||
final greyStyle = TextStyle(
|
final greyStyle = TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
|
||||||
@@ -239,8 +244,11 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
|
|
||||||
Widget _buildPeerCard(
|
Widget _buildPeerCard(
|
||||||
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
|
||||||
final name =
|
hideUsernameOnCard ??=
|
||||||
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y';
|
||||||
|
final name = hideUsernameOnCard == true
|
||||||
|
? peer.hostname
|
||||||
|
: '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
|
||||||
final child = Card(
|
final child = Card(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|||||||
@@ -399,9 +399,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
final peers = model.selectedPeers;
|
final peers = model.selectedPeers;
|
||||||
switch (model.currentTab) {
|
switch (model.currentTab) {
|
||||||
case 0:
|
case 0:
|
||||||
peers.map((p) async {
|
for (var p in peers) {
|
||||||
await bind.mainRemovePeer(id: p.id);
|
await bind.mainRemovePeer(id: p.id);
|
||||||
}).toList();
|
}
|
||||||
await bind.mainLoadRecentPeers();
|
await bind.mainLoadRecentPeers();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
@@ -413,9 +413,9 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
await bind.mainLoadFavPeers();
|
await bind.mainLoadFavPeers();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
peers.map((p) async {
|
for (var p in peers) {
|
||||||
await bind.mainRemoveDiscovered(id: p.id);
|
await bind.mainRemoveDiscovered(id: p.id);
|
||||||
}).toList();
|
}
|
||||||
await bind.mainLoadLanPeers();
|
await bind.mainLoadLanPeers();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
final _curPeers = <String>{};
|
final _curPeers = <String>{};
|
||||||
var _lastChangeTime = DateTime.now();
|
var _lastChangeTime = DateTime.now();
|
||||||
var _lastQueryPeers = <String>{};
|
var _lastQueryPeers = <String>{};
|
||||||
var _lastQueryTime = DateTime.now().add(const Duration(seconds: 30));
|
var _lastQueryTime = DateTime.now();
|
||||||
var _queryCount = 0;
|
var _queryCount = 0;
|
||||||
var _exit = false;
|
var _exit = false;
|
||||||
|
|
||||||
@@ -253,10 +253,14 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
final _queryInterval = const Duration(seconds: 20);
|
var _queryInterval = const Duration(seconds: 20);
|
||||||
|
|
||||||
void _startCheckOnlines() {
|
void _startCheckOnlines() {
|
||||||
() async {
|
() async {
|
||||||
|
final p = await bind.mainIsUsingPublicServer();
|
||||||
|
if (!p) {
|
||||||
|
_queryInterval = const Duration(seconds: 6);
|
||||||
|
}
|
||||||
while (!_exit) {
|
while (!_exit) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
if (!setEquals(_curPeers, _lastQueryPeers)) {
|
||||||
@@ -264,7 +268,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
_queryOnlines(false);
|
_queryOnlines(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_queryCount < _maxQueryCount) {
|
if (_queryCount < _maxQueryCount || !p) {
|
||||||
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
if (now.difference(_lastQueryTime) >= _queryInterval) {
|
||||||
if (_curPeers.isNotEmpty) {
|
if (_curPeers.isNotEmpty) {
|
||||||
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
bind.queryOnlines(ids: _curPeers.toList(growable: false));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter_hbb/common.dart';
|
|||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -22,6 +23,20 @@ class TTextMenu {
|
|||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.divider = false});
|
this.divider = false});
|
||||||
|
|
||||||
|
Widget getChild() {
|
||||||
|
if (trailingIcon != null) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
trailingIcon!,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TRadioMenu<T> {
|
class TRadioMenu<T> {
|
||||||
@@ -636,6 +651,18 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
v.addAll(toolbarKeyboardToggles(ffi));
|
v.addAll(toolbarKeyboardToggles(ffi));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// view mode (mobile only, desktop is in keyboard menu)
|
||||||
|
if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) {
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: ffiModel.viewOnly,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
await bind.sessionToggleOption(
|
||||||
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||||
|
ffiModel.setViewOnly(id, value);
|
||||||
|
},
|
||||||
|
child: Text(translate('View Mode'))));
|
||||||
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,3 +803,106 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
|||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool showVirtualDisplayMenu(FFI ffi) {
|
||||||
|
if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ffi.ffiModel.pi.isInstalled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> getVirtualDisplayMenuChildren(
|
||||||
|
FFI ffi, String id, VoidCallback? clickCallBack) {
|
||||||
|
if (!showVirtualDisplayMenu(ffi)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final pi = ffi.ffiModel.pi;
|
||||||
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
|
if (pi.isRustDeskIdd) {
|
||||||
|
final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
||||||
|
final children = <Widget>[];
|
||||||
|
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
||||||
|
children.add(Obx(() => CkbMenuButton(
|
||||||
|
value: virtualDisplays.contains(i + 1),
|
||||||
|
onChanged: privacyModeState.isNotEmpty
|
||||||
|
? null
|
||||||
|
: (bool? value) async {
|
||||||
|
if (value != null) {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: i + 1, on: value);
|
||||||
|
clickCallBack?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('${translate('Virtual display')} ${i + 1}'),
|
||||||
|
ffi: ffi,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
children.add(Divider());
|
||||||
|
children.add(Obx(() => MenuButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
index: kAllVirtualDisplay,
|
||||||
|
on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
ffi: ffi,
|
||||||
|
child: Text(translate('Plug out all')),
|
||||||
|
)));
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
if (pi.isAmyuniIdd) {
|
||||||
|
final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
||||||
|
final children = <Widget>[
|
||||||
|
Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: 0, on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
child: Icon(Icons.remove),
|
||||||
|
),
|
||||||
|
Text(count.toString()),
|
||||||
|
TextButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 4
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId, index: 0, on: true);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Divider(),
|
||||||
|
Obx(() => MenuButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
index: kAllVirtualDisplay,
|
||||||
|
on: false);
|
||||||
|
clickCallBack?.call();
|
||||||
|
},
|
||||||
|
ffi: ffi,
|
||||||
|
child: Text(translate('Plug out all')),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,6 +135,17 @@ const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
|||||||
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||||
const String kOptionStopService = "stop-service";
|
const String kOptionStopService = "stop-service";
|
||||||
const String kOptionDirectxCapture = "enable-directx-capture";
|
const String kOptionDirectxCapture = "enable-directx-capture";
|
||||||
|
const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
|
||||||
|
|
||||||
|
// buildin opitons
|
||||||
|
const String kOptionHideServerSetting = "hide-server-settings";
|
||||||
|
const String kOptionHideProxySetting = "hide-proxy-settings";
|
||||||
|
const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||||
|
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||||
|
const String kOptionRemovePresetPasswordWarning =
|
||||||
|
"remove-preset-password-warning";
|
||||||
|
const kHideUsernameOnCard = "hide-username-on-card";
|
||||||
|
const String kOptionHideHelpCards = "hide-help-cards";
|
||||||
|
|
||||||
const String kOptionToggleViewOnly = "view-only";
|
const String kOptionToggleViewOnly = "view-only";
|
||||||
|
|
||||||
@@ -155,6 +166,8 @@ const int kWindowMainId = 0;
|
|||||||
const String kPointerEventKindTouch = "touch";
|
const String kPointerEventKindTouch = "touch";
|
||||||
const String kPointerEventKindMouse = "mouse";
|
const String kPointerEventKindMouse = "mouse";
|
||||||
|
|
||||||
|
const String kKeyFlutterKey = "flutter_key";
|
||||||
|
|
||||||
const String kKeyShowDisplaysAsIndividualWindows =
|
const String kKeyShowDisplaysAsIndividualWindows =
|
||||||
'displays_as_individual_windows';
|
'displays_as_individual_windows';
|
||||||
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
?.merge(TextStyle(height: 1)),
|
?.merge(TextStyle(height: 1)),
|
||||||
).marginOnly(right: 4),
|
).marginOnly(right: 4),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
waitDuration: Duration(milliseconds: 0),
|
waitDuration: Duration(milliseconds: 300),
|
||||||
message: translate("id_input_tip"),
|
message: translate("id_input_tip"),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.help_outline_outlined,
|
Icons.help_outline_outlined,
|
||||||
|
|||||||
@@ -443,14 +443,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (isMacOS) {
|
} else if (isMacOS) {
|
||||||
if (!(bind.isOutgoingOnly() ||
|
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||||
bind.mainIsCanScreenRecording(prompt: false))) {
|
if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
|
||||||
return buildInstallCard("Permissions", "config_screen", "Configure",
|
return buildInstallCard("Permissions", "config_screen", "Configure",
|
||||||
() async {
|
() async {
|
||||||
bind.mainIsCanScreenRecording(prompt: true);
|
bind.mainIsCanScreenRecording(prompt: true);
|
||||||
watchIsCanScreenRecording = true;
|
watchIsCanScreenRecording = true;
|
||||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||||
} else if (!bind.mainIsProcessTrusted(prompt: false)) {
|
} else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
|
||||||
return buildInstallCard("Permissions", "config_acc", "Configure",
|
return buildInstallCard("Permissions", "config_acc", "Configure",
|
||||||
() async {
|
() async {
|
||||||
bind.mainIsProcessTrusted(prompt: true);
|
bind.mainIsProcessTrusted(prompt: true);
|
||||||
@@ -462,7 +462,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
bind.mainIsCanInputMonitoring(prompt: true);
|
bind.mainIsCanInputMonitoring(prompt: true);
|
||||||
watchIsInputMonitoring = true;
|
watchIsInputMonitoring = true;
|
||||||
}, help: 'Help', link: translate("doc_mac_permission"));
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
||||||
} else if (!svcStopped.value &&
|
} else if (!isOutgoingOnly &&
|
||||||
|
!svcStopped.value &&
|
||||||
bind.mainIsInstalled() &&
|
bind.mainIsInstalled() &&
|
||||||
!bind.mainIsInstalledDaemon(prompt: false)) {
|
!bind.mainIsInstalledDaemon(prompt: false)) {
|
||||||
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
||||||
@@ -545,6 +546,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
String? link,
|
String? link,
|
||||||
bool? closeButton,
|
bool? closeButton,
|
||||||
String? closeOption}) {
|
String? closeOption}) {
|
||||||
|
if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
|
||||||
|
content != 'install_daemon_tip') {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
void closeCard() async {
|
void closeCard() async {
|
||||||
if (closeOption != null) {
|
if (closeOption != null) {
|
||||||
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
||||||
@@ -838,7 +843,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPasswordDialog() async {
|
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
|
||||||
final pw = await bind.mainGetPermanentPassword();
|
final pw = await bind.mainGetPermanentPassword();
|
||||||
final p0 = TextEditingController(text: pw);
|
final p0 = TextEditingController(text: pw);
|
||||||
final p1 = TextEditingController(text: pw);
|
final p1 = TextEditingController(text: pw);
|
||||||
@@ -878,6 +883,9 @@ void setPasswordDialog() async {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bind.mainSetPermanentPassword(password: pass);
|
bind.mainSetPermanentPassword(password: pass);
|
||||||
|
if (pass.isNotEmpty) {
|
||||||
|
notEmptyCallback?.call();
|
||||||
|
}
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,13 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
final SettingsTabKey initialTabkey;
|
final SettingsTabKey initialTabkey;
|
||||||
static final List<SettingsTabKey> tabKeys = [
|
static final List<SettingsTabKey> tabKeys = [
|
||||||
SettingsTabKey.general,
|
SettingsTabKey.general,
|
||||||
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
|
if (!bind.isOutgoingOnly() &&
|
||||||
|
!bind.isDisableSettings() &&
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
||||||
SettingsTabKey.safety,
|
SettingsTabKey.safety,
|
||||||
if (!bind.isDisableSettings()) SettingsTabKey.network,
|
if (!bind.isDisableSettings() &&
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
|
||||||
|
SettingsTabKey.network,
|
||||||
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
if (!bind.isIncomingOnly()) SettingsTabKey.display,
|
||||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
||||||
SettingsTabKey.plugin,
|
SettingsTabKey.plugin,
|
||||||
@@ -84,8 +88,10 @@ class DesktopSettingPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
|
||||||
DesktopTabPage.onAddSetting(initialPage: page);
|
DesktopTabPage.onAddSetting(initialPage: page);
|
||||||
PageController controller = Get.find(tag: _kSettingPageControllerTag);
|
PageController controller =
|
||||||
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
|
Get.find<PageController>(tag: _kSettingPageControllerTag);
|
||||||
|
Rx<SettingsTabKey> selected =
|
||||||
|
Get.find<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
|
||||||
selected.value = page;
|
selected.value = page;
|
||||||
controller.jumpToPage(index);
|
controller.jumpToPage(index);
|
||||||
} else {
|
} else {
|
||||||
@@ -171,16 +177,32 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _children() {
|
List<Widget> _children() {
|
||||||
final children = [
|
final children = List<Widget>.empty(growable: true);
|
||||||
_General(),
|
for (final tab in DesktopSettingPage.tabKeys) {
|
||||||
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(),
|
switch (tab) {
|
||||||
if (!bind.isDisableSettings()) _Network(),
|
case SettingsTabKey.general:
|
||||||
if (!bind.isIncomingOnly()) _Display(),
|
children.add(const _General());
|
||||||
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
|
break;
|
||||||
_Plugin(),
|
case SettingsTabKey.safety:
|
||||||
if (!bind.isDisableAccount()) _Account(),
|
children.add(const _Safety());
|
||||||
_About(),
|
break;
|
||||||
];
|
case SettingsTabKey.network:
|
||||||
|
children.add(const _Network());
|
||||||
|
break;
|
||||||
|
case SettingsTabKey.display:
|
||||||
|
children.add(const _Display());
|
||||||
|
break;
|
||||||
|
case SettingsTabKey.plugin:
|
||||||
|
children.add(const _Plugin());
|
||||||
|
break;
|
||||||
|
case SettingsTabKey.account:
|
||||||
|
children.add(const _Account());
|
||||||
|
break;
|
||||||
|
case SettingsTabKey.about:
|
||||||
|
children.add(const _About());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +515,7 @@ class _GeneralState extends State<_General> {
|
|||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
return AudioInput(builder: (devices, currentDevice, setDevice) {
|
builder(devices, currentDevice, setDevice) {
|
||||||
return _Card(title: 'Audio Input Device', children: [
|
return _Card(title: 'Audio Input Device', children: [
|
||||||
...devices.map((device) => _Radio<String>(context,
|
...devices.map((device) => _Radio<String>(context,
|
||||||
value: device,
|
value: device,
|
||||||
@@ -504,7 +526,9 @@ class _GeneralState extends State<_General> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}))
|
}))
|
||||||
]);
|
]);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget record(BuildContext context) {
|
Widget record(BuildContext context) {
|
||||||
@@ -679,15 +703,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
// Simple temp wrapper for PR check
|
// Simple temp wrapper for PR check
|
||||||
tmpWrapper() {
|
tmpWrapper() {
|
||||||
RxBool has2fa = bind.mainHasValid2FaSync().obs;
|
RxBool has2fa = bind.mainHasValid2FaSync().obs;
|
||||||
|
RxBool hasBot = bind.mainHasValidBotSync().obs;
|
||||||
update() async {
|
update() async {
|
||||||
has2fa.value = bind.mainHasValid2FaSync();
|
has2fa.value = bind.mainHasValid2FaSync();
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChanged(bool? checked) async {
|
onChanged(bool? checked) async {
|
||||||
change2fa(callback: update);
|
if (checked == false) {
|
||||||
|
CommonConfirmDialog(
|
||||||
|
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
||||||
|
change2fa(callback: update);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
change2fa(callback: update);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
final tfa = GestureDetector(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
child: Obx(() => Row(
|
child: Obx(() => Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -708,6 +741,52 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
onChanged(!has2fa.value);
|
onChanged(!has2fa.value);
|
||||||
},
|
},
|
||||||
).marginOnly(left: _kCheckBoxLeftMargin);
|
).marginOnly(left: _kCheckBoxLeftMargin);
|
||||||
|
if (!has2fa.value) {
|
||||||
|
return tfa;
|
||||||
|
}
|
||||||
|
updateBot() async {
|
||||||
|
hasBot.value = bind.mainHasValidBotSync();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangedBot(bool? checked) async {
|
||||||
|
if (checked == false) {
|
||||||
|
CommonConfirmDialog(
|
||||||
|
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
|
||||||
|
changeBot(callback: updateBot);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
changeBot(callback: updateBot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final bot = GestureDetector(
|
||||||
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 300),
|
||||||
|
message: translate("enable-bot-tip"),
|
||||||
|
child: InkWell(
|
||||||
|
child: Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: hasBot.value,
|
||||||
|
onChanged: enabled ? onChangedBot : null)
|
||||||
|
.marginOnly(right: 5),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
translate('Telegram bot'),
|
||||||
|
style: TextStyle(
|
||||||
|
color: disabledTextColor(context, enabled)),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
onChangedBot(!hasBot.value);
|
||||||
|
},
|
||||||
|
).marginOnly(left: _kCheckBoxLeftMargin + 30);
|
||||||
|
return Column(
|
||||||
|
children: [tfa, bot],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmpWrapper();
|
return tmpWrapper();
|
||||||
@@ -832,12 +911,22 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
label: value,
|
label: value,
|
||||||
onChanged: locked
|
onChanged: locked
|
||||||
? null
|
? null
|
||||||
: ((value) {
|
: ((value) async {
|
||||||
() async {
|
callback() async {
|
||||||
await model.setVerificationMethod(
|
await model.setVerificationMethod(
|
||||||
passwordKeys[passwordValues.indexOf(value)]);
|
passwordKeys[passwordValues.indexOf(value)]);
|
||||||
await model.updatePasswordModel();
|
await model.updatePasswordModel();
|
||||||
}();
|
}
|
||||||
|
|
||||||
|
if (value ==
|
||||||
|
passwordValues[passwordKeys
|
||||||
|
.indexOf(kUsePermanentPassword)] &&
|
||||||
|
(await bind.mainGetPermanentPassword())
|
||||||
|
.isEmpty) {
|
||||||
|
setPasswordDialog(notEmptyCallback: callback);
|
||||||
|
} else {
|
||||||
|
await callback();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -1030,12 +1119,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
bool enabled = !locked;
|
bool enabled = !locked;
|
||||||
// Simple temp wrapper for PR check
|
// Simple temp wrapper for PR check
|
||||||
tmpWrapper() {
|
tmpWrapper() {
|
||||||
RxBool hasWhitelist = (bind.mainGetOptionSync(key: kOptionWhitelist) !=
|
RxBool hasWhitelist = whitelistNotEmpty().obs;
|
||||||
defaultOptionWhitelist)
|
|
||||||
.obs;
|
|
||||||
update() async {
|
update() async {
|
||||||
hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) !=
|
hasWhitelist.value = whitelistNotEmpty();
|
||||||
defaultOptionWhitelist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChanged(bool? checked) async {
|
onChanged(bool? checked) async {
|
||||||
@@ -1146,7 +1232,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
|||||||
width: 95,
|
width: 95,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: enabled && !locked && isOptFixed,
|
enabled: enabled && !locked && !isOptFixed,
|
||||||
onChanged: (_) => applyEnabled.value = true,
|
onChanged: (_) => applyEnabled.value = true,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(RegExp(
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
@@ -1199,6 +1285,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
|||||||
super.build(context);
|
super.build(context);
|
||||||
bool enabled = !locked;
|
bool enabled = !locked;
|
||||||
final scrollController = ScrollController();
|
final scrollController = ScrollController();
|
||||||
|
final hideServer =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||||
|
final hideProxy =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||||
return DesktopScrollWrapper(
|
return DesktopScrollWrapper(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@@ -1212,11 +1302,12 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
|||||||
AbsorbPointer(
|
AbsorbPointer(
|
||||||
absorbing: locked,
|
absorbing: locked,
|
||||||
child: Column(children: [
|
child: Column(children: [
|
||||||
server(enabled),
|
if (!hideServer) server(enabled),
|
||||||
_Card(title: 'Proxy', children: [
|
if (!hideProxy)
|
||||||
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
_Card(title: 'Proxy', children: [
|
||||||
enabled: enabled),
|
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
||||||
]),
|
enabled: enabled),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
]).marginOnly(bottom: _kListViewBottomMargin));
|
]).marginOnly(bottom: _kListViewBottomMargin));
|
||||||
@@ -1715,7 +1806,7 @@ class _AboutState extends State<_About> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
physics: DraggableNeverScrollableScrollPhysics(),
|
||||||
child: _Card(title: '${translate('About')} RustDesk', children: [
|
child: _Card(title: translate('About RustDesk'), children: [
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -2249,35 +2340,40 @@ void changeSocks5Proxy() async {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
if (!isMobile)
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
ConstrainedBox(
|
||||||
child: Align(
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
alignment: Alignment.centerRight,
|
child: Align(
|
||||||
child: Row(
|
alignment: Alignment.centerRight,
|
||||||
children: [
|
child: Row(
|
||||||
Text(
|
children: [
|
||||||
translate('Server'),
|
Text(
|
||||||
).marginOnly(right: 4),
|
translate('Server'),
|
||||||
Tooltip(
|
).marginOnly(right: 4),
|
||||||
waitDuration: Duration(milliseconds: 0),
|
Tooltip(
|
||||||
message: translate("default_proxy_tip"),
|
waitDuration: Duration(milliseconds: 0),
|
||||||
child: Icon(
|
message: translate("default_proxy_tip"),
|
||||||
Icons.help_outline_outlined,
|
child: Icon(
|
||||||
size: 16,
|
Icons.help_outline_outlined,
|
||||||
color: Theme.of(context)
|
size: 16,
|
||||||
.textTheme
|
color: Theme.of(context)
|
||||||
.titleLarge
|
.textTheme
|
||||||
?.color
|
.titleLarge
|
||||||
?.withOpacity(0.5),
|
?.color
|
||||||
|
?.withOpacity(0.5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
)).marginOnly(right: 10),
|
||||||
)).marginOnly(right: 10),
|
),
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
|
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
|
||||||
|
labelText: isMobile ? translate('Server') : null,
|
||||||
|
helperText:
|
||||||
|
isMobile ? translate("default_proxy_tip") : null,
|
||||||
|
helperMaxLines: isMobile ? 3 : null,
|
||||||
),
|
),
|
||||||
controller: proxyController,
|
controller: proxyController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -2288,15 +2384,19 @@ void changeSocks5Proxy() async {
|
|||||||
).marginOnly(bottom: 8),
|
).marginOnly(bottom: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
if (!isMobile)
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
ConstrainedBox(
|
||||||
child: Text(
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
'${translate("Username")}:',
|
child: Text(
|
||||||
textAlign: TextAlign.right,
|
'${translate("Username")}:',
|
||||||
).marginOnly(right: 10)),
|
textAlign: TextAlign.right,
|
||||||
|
).marginOnly(right: 10)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: userController,
|
controller: userController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: isMobile ? translate('Username') : null,
|
||||||
|
),
|
||||||
enabled: !isOptFixed,
|
enabled: !isOptFixed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -2304,16 +2404,18 @@ void changeSocks5Proxy() async {
|
|||||||
).marginOnly(bottom: 8),
|
).marginOnly(bottom: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
if (!isMobile)
|
||||||
constraints: const BoxConstraints(minWidth: 140),
|
ConstrainedBox(
|
||||||
child: Text(
|
constraints: const BoxConstraints(minWidth: 140),
|
||||||
'${translate("Password")}:',
|
child: Text(
|
||||||
textAlign: TextAlign.right,
|
'${translate("Password")}:',
|
||||||
).marginOnly(right: 10)),
|
textAlign: TextAlign.right,
|
||||||
|
).marginOnly(right: 10)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() => TextField(
|
child: Obx(() => TextField(
|
||||||
obscureText: obscure.value,
|
obscureText: obscure.value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
labelText: isMobile ? translate('Password') : null,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: () => obscure.value = !obscure.value,
|
onPressed: () => obscure.value = !obscure.value,
|
||||||
icon: Icon(obscure.value
|
icon: Icon(obscure.value
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
|||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
// import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
static void onAddSetting(
|
static void onAddSetting(
|
||||||
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
||||||
try {
|
try {
|
||||||
DesktopTabController tabController = Get.find();
|
DesktopTabController tabController = Get.find<DesktopTabController>();
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: kTabLabelSettingPage,
|
key: kTabLabelSettingPage,
|
||||||
label: kTabLabelSettingPage,
|
label: kTabLabelSettingPage,
|
||||||
@@ -41,6 +42,7 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||||
|
|
||||||
final RxBool _block = false.obs;
|
final RxBool _block = false.obs;
|
||||||
|
// bool mouseIn = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
@@ -53,6 +55,7 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
Get.put<DesktopTabController>(tabController);
|
Get.put<DesktopTabController>(tabController);
|
||||||
RemoteCountState.init();
|
RemoteCountState.init();
|
||||||
@@ -78,8 +81,19 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
bool _handleKeyEvent(KeyEvent event) {
|
||||||
|
if (!mouseIn && event is KeyDownEvent) {
|
||||||
|
print('key down: ${event.logicalKey}');
|
||||||
|
shouldBeBlocked(_block, canBeBlocked);
|
||||||
|
}
|
||||||
|
return false; // allow it to propagate
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
// HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
Get.delete<DesktopTabController>();
|
Get.delete<DesktopTabController>();
|
||||||
|
|
||||||
@@ -102,18 +116,14 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
isClose: false,
|
isClose: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
blockTab: _block,
|
||||||
)));
|
)));
|
||||||
widget() => MouseRegion(
|
|
||||||
onEnter: (_) async {
|
|
||||||
await shouldBeBlocked(_block, canBeBlocked);
|
|
||||||
},
|
|
||||||
child: FocusScope(child: tabWidget, canRequestFocus: !_block.value));
|
|
||||||
return isMacOS || kUseCompatibleUiMode
|
return isMacOS || kUseCompatibleUiMode
|
||||||
? Obx(() => widget())
|
? tabWidget
|
||||||
: Obx(
|
: Obx(
|
||||||
() => DragToResizeArea(
|
() => DragToResizeArea(
|
||||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||||
child: widget(),
|
child: tabWidget,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
_ffi.dialogManager
|
_ffi.dialogManager
|
||||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||||
});
|
});
|
||||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
|
||||||
if (!isLinux) {
|
if (!isLinux) {
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
isSharedPassword: widget.isSharedPassword,
|
isSharedPassword: widget.isSharedPassword,
|
||||||
forceRelay: widget.forceRelay,
|
forceRelay: widget.forceRelay,
|
||||||
isRdp: widget.isRDP);
|
isRdp: widget.isRDP);
|
||||||
Get.put(_ffi, tag: 'pf_${widget.id}');
|
Get.put<FFI>(_ffi, tag: 'pf_${widget.id}');
|
||||||
debugPrint("Port forward page init success with id ${widget.id}");
|
debugPrint("Port forward page init success with id ${widget.id}");
|
||||||
widget.tabController.onSelected?.call(widget.id);
|
widget.tabController.onSelected?.call(widget.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_initStates(widget.id);
|
_initStates(widget.id);
|
||||||
_ffi = FFI(widget.sessionId);
|
_ffi = FFI(widget.sessionId);
|
||||||
Get.put(_ffi, tag: widget.id);
|
Get.put<FFI>(_ffi, tag: widget.id);
|
||||||
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
||||||
showKBLayoutTypeChooserIfNeeded(
|
showKBLayoutTypeChooserIfNeeded(
|
||||||
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
||||||
@@ -506,12 +506,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!_ffi.canvasModel.cursorEmbedded) {
|
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||||
paints.add(Obx(() => Offstage(
|
paints
|
||||||
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
|
.add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
|
||||||
child: CursorPaint(
|
? Offstage()
|
||||||
id: widget.id,
|
: CursorPaint(
|
||||||
zoomCursor: _zoomCursor,
|
id: widget.id,
|
||||||
))));
|
zoomCursor: _zoomCursor,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
paints.add(
|
paints.add(
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
void initState() {
|
void initState() {
|
||||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
Get.put(tabController);
|
Get.put<DesktopTabController>(tabController);
|
||||||
tabController.onRemoved = (_, id) {
|
tabController.onRemoved = (_, id) {
|
||||||
onRemoveId(id);
|
onRemoveId(id);
|
||||||
};
|
};
|
||||||
@@ -104,7 +104,20 @@ class ConnectionManager extends StatefulWidget {
|
|||||||
State<StatefulWidget> createState() => ConnectionManagerState();
|
State<StatefulWidget> createState() => ConnectionManagerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConnectionManagerState extends State<ConnectionManager> {
|
class ConnectionManagerState extends State<ConnectionManager>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
final RxBool _block = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
if (!allowRemoteCMModification()) {
|
||||||
|
shouldBeBlocked(_block, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
gFFI.serverModel.updateClientState();
|
gFFI.serverModel.updateClientState();
|
||||||
@@ -127,9 +140,16 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
gFFI.chatModel.isConnManager = true;
|
gFFI.chatModel.isConnManager = true;
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final serverModel = Provider.of<ServerModel>(context);
|
final serverModel = Provider.of<ServerModel>(context);
|
||||||
@@ -165,6 +185,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
selectedBorderColor: MyTheme.accent,
|
selectedBorderColor: MyTheme.accent,
|
||||||
maxLabelWidth: 100,
|
maxLabelWidth: 100,
|
||||||
tail: null, //buildScrollJumper(),
|
tail: null, //buildScrollJumper(),
|
||||||
|
blockTab: allowRemoteCMModification() ? null : _block,
|
||||||
selectedTabBackgroundColor:
|
selectedTabBackgroundColor:
|
||||||
Theme.of(context).hintColor.withOpacity(0),
|
Theme.of(context).hintColor.withOpacity(0),
|
||||||
tabBuilder: (key, icon, label, themeConf) {
|
tabBuilder: (key, icon, label, themeConf) {
|
||||||
@@ -207,15 +228,12 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
Consumer<ChatModel>(
|
Consumer<ChatModel>(
|
||||||
builder: (_, model, child) => SizedBox(
|
builder: (_, model, child) => SizedBox(
|
||||||
width: realChatPageWidth,
|
width: realChatPageWidth,
|
||||||
child: buildRemoteBlock(
|
child: allowRemoteCMModification()
|
||||||
child: Container(
|
? buildSidePage()
|
||||||
decoration: BoxDecoration(
|
: buildRemoteBlock(
|
||||||
border: Border(
|
child: buildSidePage(),
|
||||||
right: BorderSide(
|
block: _block,
|
||||||
color: Theme.of(context)
|
mask: true),
|
||||||
.dividerColor))),
|
|
||||||
child: buildSidePage()),
|
|
||||||
),
|
|
||||||
)),
|
)),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: realClosedWidth,
|
width: realClosedWidth,
|
||||||
@@ -714,7 +732,7 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
child: buildButton(context,
|
child: buildButton(context,
|
||||||
color: MyTheme.accent,
|
color: MyTheme.accent,
|
||||||
onClick: null, onTapDown: (details) async {
|
onClick: null, onTapDown: (details) async {
|
||||||
final devicesInfo = await AudioInput.getDevicesInfo();
|
final devicesInfo = await AudioInput.getDevicesInfo(true, true);
|
||||||
List<String> devices = devicesInfo['devices'] as List<String>;
|
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||||
if (devices.isEmpty) {
|
if (devices.isEmpty) {
|
||||||
msgBox(
|
msgBox(
|
||||||
@@ -740,13 +758,13 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
value: d,
|
value: d,
|
||||||
height: 18,
|
height: 18,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onTap: () => AudioInput.setDevice(d),
|
onTap: () => AudioInput.setDevice(d, true, true),
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: RadioMenuButton(
|
child: RadioMenuButton(
|
||||||
value: d,
|
value: d,
|
||||||
groupValue: currentDevice,
|
groupValue: currentDevice,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
if (v != null) AudioInput.setDevice(v);
|
if (v != null) AudioInput.setDevice(v, true, true);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1043,6 +1061,10 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void checkClickTime(int id, Function() callback) async {
|
void checkClickTime(int id, Function() callback) async {
|
||||||
|
if (allowRemoteCMModification()) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
await bind.cmCheckClickTime(connId: id);
|
await bind.cmCheckClickTime(connId: id);
|
||||||
Timer(const Duration(milliseconds: 120), () async {
|
Timer(const Duration(milliseconds: 120), () async {
|
||||||
@@ -1051,6 +1073,11 @@ void checkClickTime(int id, Function() callback) async {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool allowRemoteCMModification() {
|
||||||
|
return option2bool(kOptionAllowRemoteCmModification,
|
||||||
|
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
|
||||||
|
}
|
||||||
|
|
||||||
class _FileTransferLogPage extends StatefulWidget {
|
class _FileTransferLogPage extends StatefulWidget {
|
||||||
_FileTransferLogPage({Key? key}) : super(key: key);
|
_FileTransferLogPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
|||||||
@@ -1052,15 +1052,11 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
screenAdjustor: _screenAdjustor,
|
screenAdjustor: _screenAdjustor,
|
||||||
),
|
),
|
||||||
if (pi.isRustDeskIdd)
|
if (showVirtualDisplayMenu(ffi))
|
||||||
_RustDeskVirtualDisplayMenu(
|
_SubmenuButton(
|
||||||
id: widget.id,
|
|
||||||
ffi: widget.ffi,
|
|
||||||
),
|
|
||||||
if (pi.isAmyuniIdd)
|
|
||||||
_AmyuniVirtualDisplayMenu(
|
|
||||||
id: widget.id,
|
|
||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
|
menuChildren: getVirtualDisplayMenuChildren(ffi, id, null),
|
||||||
|
child: Text(translate("Virtual display")),
|
||||||
),
|
),
|
||||||
cursorToggles(),
|
cursorToggles(),
|
||||||
Divider(),
|
Divider(),
|
||||||
@@ -1559,155 +1555,6 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
|
|
||||||
final String id;
|
|
||||||
final FFI ffi;
|
|
||||||
|
|
||||||
_RustDeskVirtualDisplayMenu({
|
|
||||||
Key? key,
|
|
||||||
required this.id,
|
|
||||||
required this.ffi,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_RustDeskVirtualDisplayMenu> createState() =>
|
|
||||||
_RustDeskVirtualDisplayMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RustDeskVirtualDisplayMenuState
|
|
||||||
extends State<_RustDeskVirtualDisplayMenu> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
|
||||||
return Offstage();
|
|
||||||
}
|
|
||||||
if (!widget.ffi.ffiModel.pi.isInstalled) {
|
|
||||||
return Offstage();
|
|
||||||
}
|
|
||||||
|
|
||||||
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
|
||||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
|
||||||
|
|
||||||
final children = <Widget>[];
|
|
||||||
for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
|
|
||||||
children.add(Obx(() => CkbMenuButton(
|
|
||||||
value: virtualDisplays.contains(i + 1),
|
|
||||||
onChanged: privacyModeState.isNotEmpty
|
|
||||||
? null
|
|
||||||
: (bool? value) async {
|
|
||||||
if (value != null) {
|
|
||||||
bind.sessionToggleVirtualDisplay(
|
|
||||||
sessionId: widget.ffi.sessionId,
|
|
||||||
index: i + 1,
|
|
||||||
on: value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('${translate('Virtual display')} ${i + 1}'),
|
|
||||||
ffi: widget.ffi,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
children.add(Divider());
|
|
||||||
children.add(Obx(() => MenuButton(
|
|
||||||
onPressed: privacyModeState.isNotEmpty
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
bind.sessionToggleVirtualDisplay(
|
|
||||||
sessionId: widget.ffi.sessionId,
|
|
||||||
index: kAllVirtualDisplay,
|
|
||||||
on: false);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Plug out all')),
|
|
||||||
)));
|
|
||||||
return _SubmenuButton(
|
|
||||||
ffi: widget.ffi,
|
|
||||||
menuChildren: children,
|
|
||||||
child: Text(translate("Virtual display")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
|
|
||||||
final String id;
|
|
||||||
final FFI ffi;
|
|
||||||
|
|
||||||
_AmyuniVirtualDisplayMenu({
|
|
||||||
Key? key,
|
|
||||||
required this.id,
|
|
||||||
required this.ffi,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_AmyuniVirtualDisplayMenu> createState() =>
|
|
||||||
_AmiyuniVirtualDisplayMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
|
||||||
return Offstage();
|
|
||||||
}
|
|
||||||
if (!widget.ffi.ffiModel.pi.isInstalled) {
|
|
||||||
return Offstage();
|
|
||||||
}
|
|
||||||
|
|
||||||
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
|
||||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
|
||||||
|
|
||||||
final children = <Widget>[
|
|
||||||
Obx(() => Row(
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
|
||||||
? null
|
|
||||||
: () => bind.sessionToggleVirtualDisplay(
|
|
||||||
sessionId: widget.ffi.sessionId, index: 0, on: false),
|
|
||||||
child: Icon(Icons.remove),
|
|
||||||
),
|
|
||||||
Text(count.toString()),
|
|
||||||
TextButton(
|
|
||||||
onPressed: privacyModeState.isNotEmpty || count == 4
|
|
||||||
? null
|
|
||||||
: () => bind.sessionToggleVirtualDisplay(
|
|
||||||
sessionId: widget.ffi.sessionId, index: 0, on: true),
|
|
||||||
child: Icon(Icons.add),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
Divider(),
|
|
||||||
Obx(() => MenuButton(
|
|
||||||
onPressed: privacyModeState.isNotEmpty || count == 0
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
bind.sessionToggleVirtualDisplay(
|
|
||||||
sessionId: widget.ffi.sessionId,
|
|
||||||
index: kAllVirtualDisplay,
|
|
||||||
on: false);
|
|
||||||
},
|
|
||||||
ffi: widget.ffi,
|
|
||||||
child: Text(translate('Plug out all')),
|
|
||||||
)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return _SubmenuButton(
|
|
||||||
ffi: widget.ffi,
|
|
||||||
menuChildren: children,
|
|
||||||
child: Text(translate("Virtual display")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _KeyboardMenu extends StatelessWidget {
|
class _KeyboardMenu extends StatelessWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
@@ -1741,6 +1588,7 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
viewMode(),
|
viewMode(),
|
||||||
Divider(),
|
Divider(),
|
||||||
...toolbarToggles(),
|
...toolbarToggles(),
|
||||||
|
...mobileActions(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1877,6 +1725,39 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
child: Text(translate('View Mode')));
|
child: Text(translate('View Mode')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mobileActions() {
|
||||||
|
if (pi.platform != kPeerPlatformAndroid) return [];
|
||||||
|
final enabled = versionCmp(pi.version, '1.2.7') >= 0;
|
||||||
|
if (!enabled) return [];
|
||||||
|
return [
|
||||||
|
Divider(),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Back')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileBack(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Home')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileHome(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Apps')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileApps(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Volume up')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileVolumeUp(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Volume down')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobileVolumeDown(),
|
||||||
|
ffi: ffi),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('Power')),
|
||||||
|
onPressed: () => ffi.inputModel.onMobilePower(),
|
||||||
|
ffi: ffi),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatMenu extends StatefulWidget {
|
class _ChatMenu extends StatefulWidget {
|
||||||
@@ -1950,28 +1831,31 @@ class _VoiceCallMenu extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
menuChildrenGetter() {
|
menuChildrenGetter() {
|
||||||
final audioInput =
|
final audioInput = AudioInput(
|
||||||
AudioInput(builder: (devices, currentDevice, setDevice) {
|
builder: (devices, currentDevice, setDevice) {
|
||||||
return Column(
|
return Column(
|
||||||
children: devices
|
children: devices
|
||||||
.map((d) => RdoMenuButton<String>(
|
.map((d) => RdoMenuButton<String>(
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
d,
|
d,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(maxWidth: 250),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints(maxWidth: 250),
|
value: d,
|
||||||
),
|
groupValue: currentDevice,
|
||||||
value: d,
|
onChanged: (v) {
|
||||||
groupValue: currentDevice,
|
if (v != null) setDevice(v);
|
||||||
onChanged: (v) {
|
},
|
||||||
if (v != null) setDevice(v);
|
ffi: ffi,
|
||||||
},
|
))
|
||||||
ffi: ffi,
|
.toList(),
|
||||||
))
|
);
|
||||||
.toList(),
|
},
|
||||||
);
|
isCm: false,
|
||||||
});
|
isVoiceCall: true,
|
||||||
|
);
|
||||||
return [
|
return [
|
||||||
audioInput,
|
audioInput,
|
||||||
Divider(),
|
Divider(),
|
||||||
|
|||||||
@@ -230,8 +230,7 @@ typedef LabelGetter = Rx<String> Function(String key);
|
|||||||
int _lastClickTime =
|
int _lastClickTime =
|
||||||
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
class DesktopTab extends StatefulWidget {
|
||||||
class DesktopTab extends StatelessWidget {
|
|
||||||
final bool showLogo;
|
final bool showLogo;
|
||||||
final bool showTitle;
|
final bool showTitle;
|
||||||
final bool showMinimize;
|
final bool showMinimize;
|
||||||
@@ -248,15 +247,12 @@ class DesktopTab extends StatelessWidget {
|
|||||||
final Color? selectedTabBackgroundColor;
|
final Color? selectedTabBackgroundColor;
|
||||||
final Color? unSelectedTabBackgroundColor;
|
final Color? unSelectedTabBackgroundColor;
|
||||||
final Color? selectedBorderColor;
|
final Color? selectedBorderColor;
|
||||||
|
final RxBool? blockTab;
|
||||||
|
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
|
|
||||||
Rx<DesktopTabState> get state => controller.state;
|
|
||||||
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
|
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
|
||||||
|
|
||||||
late final DesktopTabType tabType;
|
|
||||||
late final bool isMainWindow;
|
|
||||||
|
|
||||||
final RxList<String> invisibleTabKeys = RxList.empty();
|
final RxList<String> invisibleTabKeys = RxList.empty();
|
||||||
|
|
||||||
DesktopTab({
|
DesktopTab({
|
||||||
@@ -277,25 +273,240 @@ class DesktopTab extends StatelessWidget {
|
|||||||
this.selectedTabBackgroundColor,
|
this.selectedTabBackgroundColor,
|
||||||
this.unSelectedTabBackgroundColor,
|
this.unSelectedTabBackgroundColor,
|
||||||
this.selectedBorderColor,
|
this.selectedBorderColor,
|
||||||
}) : super(key: key) {
|
this.blockTab,
|
||||||
tabType = controller.tabType;
|
}) : super(key: key);
|
||||||
isMainWindow = tabType == DesktopTabType.main ||
|
|
||||||
tabType == DesktopTabType.cm ||
|
|
||||||
tabType == DesktopTabType.install;
|
|
||||||
}
|
|
||||||
|
|
||||||
static RxString tablabelGetter(String peerId) {
|
static RxString tablabelGetter(String peerId) {
|
||||||
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||||
return RxString(getDesktopTabLabel(peerId, alias));
|
return RxString(getDesktopTabLabel(peerId, alias));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DesktopTab> createState() {
|
||||||
|
return _DesktopTabState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class _DesktopTabState extends State<DesktopTab>
|
||||||
|
with MultiWindowListener, WindowListener {
|
||||||
|
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
|
||||||
|
Timer? _macOSCheckRestoreTimer;
|
||||||
|
int _macOSCheckRestoreCounter = 0;
|
||||||
|
|
||||||
|
bool get showLogo => widget.showLogo;
|
||||||
|
bool get showTitle => widget.showTitle;
|
||||||
|
bool get showMinimize => widget.showMinimize;
|
||||||
|
bool get showMaximize => widget.showMaximize;
|
||||||
|
bool get showClose => widget.showClose;
|
||||||
|
Widget Function(Widget pageView)? get pageViewBuilder =>
|
||||||
|
widget.pageViewBuilder;
|
||||||
|
TabMenuBuilder? get tabMenuBuilder => widget.tabMenuBuilder;
|
||||||
|
Widget? get tail => widget.tail;
|
||||||
|
Future<bool> Function()? get onWindowCloseButton =>
|
||||||
|
widget.onWindowCloseButton;
|
||||||
|
TabBuilder? get tabBuilder => widget.tabBuilder;
|
||||||
|
LabelGetter? get labelGetter => widget.labelGetter;
|
||||||
|
double? get maxLabelWidth => widget.maxLabelWidth;
|
||||||
|
Color? get selectedTabBackgroundColor => widget.selectedTabBackgroundColor;
|
||||||
|
Color? get unSelectedTabBackgroundColor =>
|
||||||
|
widget.unSelectedTabBackgroundColor;
|
||||||
|
Color? get selectedBorderColor => widget.selectedBorderColor;
|
||||||
|
RxBool? get blockTab => widget.blockTab;
|
||||||
|
DesktopTabController get controller => widget.controller;
|
||||||
|
RxList<String> get invisibleTabKeys => widget.invisibleTabKeys;
|
||||||
|
Debouncer get _scrollDebounce => widget._scrollDebounce;
|
||||||
|
|
||||||
|
Rx<DesktopTabState> get state => controller.state;
|
||||||
|
|
||||||
|
DesktopTabType get tabType => controller.tabType;
|
||||||
|
bool get isMainWindow =>
|
||||||
|
tabType == DesktopTabType.main ||
|
||||||
|
tabType == DesktopTabType.cm ||
|
||||||
|
tabType == DesktopTabType.install;
|
||||||
|
|
||||||
|
_DesktopTabState() : super();
|
||||||
|
|
||||||
|
static RxString tablabelGetter(String peerId) {
|
||||||
|
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
|
||||||
|
return RxString(getDesktopTabLabel(peerId, alias));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
DesktopMultiWindow.addListener(this);
|
||||||
|
windowManager.addListener(this);
|
||||||
|
|
||||||
|
Future.delayed(Duration(milliseconds: 500), () {
|
||||||
|
if (isMainWindow) {
|
||||||
|
windowManager.isMaximized().then((maximized) {
|
||||||
|
if (stateGlobal.isMaximized.value != maximized) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => setState(() => stateGlobal.setMaximized(maximized)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
final wc = WindowController.fromWindowId(kWindowId!);
|
||||||
|
wc.isMaximized().then((maximized) {
|
||||||
|
debugPrint("isMaximized $maximized");
|
||||||
|
if (stateGlobal.isMaximized.value != maximized) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => setState(() => stateGlobal.setMaximized(maximized)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
DesktopMultiWindow.removeListener(this);
|
||||||
|
windowManager.removeListener(this);
|
||||||
|
_macOSCheckRestoreTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setMaximized(bool maximize) {
|
||||||
|
stateGlobal.setMaximized(maximize);
|
||||||
|
_saveFrameDebounce.call(_saveFrame);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowFocus() {
|
||||||
|
stateGlobal.isFocused.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowBlur() {
|
||||||
|
stateGlobal.isFocused.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMinimize() {
|
||||||
|
stateGlobal.setMinimized(true);
|
||||||
|
stateGlobal.setMaximized(false);
|
||||||
|
super.onWindowMinimize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMaximize() {
|
||||||
|
stateGlobal.setMinimized(false);
|
||||||
|
_setMaximized(true);
|
||||||
|
super.onWindowMaximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowUnmaximize() {
|
||||||
|
stateGlobal.setMinimized(false);
|
||||||
|
_setMaximized(false);
|
||||||
|
super.onWindowUnmaximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveFrame() async {
|
||||||
|
if (tabType == DesktopTabType.main) {
|
||||||
|
await saveWindowPosition(WindowType.Main);
|
||||||
|
} else if (kWindowType != null && kWindowId != null) {
|
||||||
|
await saveWindowPosition(kWindowType!, windowId: kWindowId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMoved() {
|
||||||
|
_saveFrameDebounce.call(_saveFrame);
|
||||||
|
super.onWindowMoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowResized() {
|
||||||
|
_saveFrameDebounce.call(_saveFrame);
|
||||||
|
super.onWindowMoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowClose() async {
|
||||||
|
mainWindowClose() async => await windowManager.hide();
|
||||||
|
notMainWindowClose(WindowController windowController) async {
|
||||||
|
if (controller.length != 0) {
|
||||||
|
debugPrint("close not empty multiwindow from taskbar");
|
||||||
|
if (isWindows) {
|
||||||
|
await windowController.show();
|
||||||
|
await windowController.focus();
|
||||||
|
final res = await onWindowCloseButton?.call() ?? true;
|
||||||
|
if (!res) return;
|
||||||
|
}
|
||||||
|
controller.clear();
|
||||||
|
}
|
||||||
|
await windowController.hide();
|
||||||
|
await rustDeskWinManager
|
||||||
|
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
||||||
|
}
|
||||||
|
|
||||||
|
macOSWindowClose(
|
||||||
|
Future<bool> Function() checkFullscreen,
|
||||||
|
Future<void> Function() closeFunc,
|
||||||
|
) async {
|
||||||
|
_macOSCheckRestoreCounter = 0;
|
||||||
|
_macOSCheckRestoreTimer =
|
||||||
|
Timer.periodic(Duration(milliseconds: 30), (timer) async {
|
||||||
|
_macOSCheckRestoreCounter++;
|
||||||
|
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
|
||||||
|
_macOSCheckRestoreTimer?.cancel();
|
||||||
|
_macOSCheckRestoreTimer = null;
|
||||||
|
Timer(Duration(milliseconds: 700), () async => await closeFunc());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide window on close
|
||||||
|
if (isMainWindow) {
|
||||||
|
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
|
||||||
|
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
|
||||||
|
}
|
||||||
|
// macOS specific workaround, the window is not hiding when in fullscreen.
|
||||||
|
if (isMacOS && await windowManager.isFullScreen()) {
|
||||||
|
await windowManager.setFullScreen(false);
|
||||||
|
await macOSWindowClose(
|
||||||
|
() async => await windowManager.isFullScreen(),
|
||||||
|
mainWindowClose,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await mainWindowClose();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// it's safe to hide the subwindow
|
||||||
|
final controller = WindowController.fromWindowId(kWindowId!);
|
||||||
|
if (isMacOS) {
|
||||||
|
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
|
||||||
|
// use ??= to make sure the value is set on first call.
|
||||||
|
|
||||||
|
if (await onWindowCloseButton?.call() ?? true) {
|
||||||
|
if (await controller.isFullScreen()) {
|
||||||
|
await controller.setFullscreen(false);
|
||||||
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
|
await macOSWindowClose(
|
||||||
|
() async => await controller.isFullScreen(),
|
||||||
|
() async => await notMainWindowClose(controller),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await notMainWindowClose(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await notMainWindowClose(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onWindowClose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(children: [
|
return Column(children: [
|
||||||
Obx(() => Offstage(
|
Obx(() {
|
||||||
offstage: !stateGlobal.showTabBar.isTrue ||
|
if (stateGlobal.showTabBar.isTrue &&
|
||||||
(kUseCompatibleUiMode && isHideSingleItem()),
|
!(kUseCompatibleUiMode && isHideSingleItem())) {
|
||||||
child: SizedBox(
|
return SizedBox(
|
||||||
height: _kTabBarHeight,
|
height: _kTabBarHeight,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -308,7 +519,11 @@ class DesktopTab extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
))),
|
);
|
||||||
|
} else {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
}),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: pageViewBuilder != null
|
child: pageViewBuilder != null
|
||||||
? pageViewBuilder!(_buildPageView())
|
? pageViewBuilder!(_buildPageView())
|
||||||
@@ -317,10 +532,15 @@ class DesktopTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBlock({required Widget child}) {
|
Widget _buildBlock({required Widget child}) {
|
||||||
if (tabType != DesktopTabType.main) {
|
if (blockTab != null) {
|
||||||
|
return buildRemoteBlock(
|
||||||
|
child: child,
|
||||||
|
block: blockTab!,
|
||||||
|
use: canBeBlocked,
|
||||||
|
mask: tabType == DesktopTabType.main);
|
||||||
|
} else {
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
return buildRemoteBlock(child: child, use: canBeBlocked);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _tabWidgets = [];
|
List<Widget> _tabWidgets = [];
|
||||||
@@ -457,7 +677,6 @@ class DesktopTab extends StatelessWidget {
|
|||||||
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
|
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
|
||||||
WindowActionPanel(
|
WindowActionPanel(
|
||||||
isMainWindow: isMainWindow,
|
isMainWindow: isMainWindow,
|
||||||
tabType: tabType,
|
|
||||||
state: state,
|
state: state,
|
||||||
tabController: controller,
|
tabController: controller,
|
||||||
invisibleTabKeys: invisibleTabKeys,
|
invisibleTabKeys: invisibleTabKeys,
|
||||||
@@ -475,7 +694,6 @@ class DesktopTab extends StatelessWidget {
|
|||||||
|
|
||||||
class WindowActionPanel extends StatefulWidget {
|
class WindowActionPanel extends StatefulWidget {
|
||||||
final bool isMainWindow;
|
final bool isMainWindow;
|
||||||
final DesktopTabType tabType;
|
|
||||||
final Rx<DesktopTabState> state;
|
final Rx<DesktopTabState> state;
|
||||||
final DesktopTabController tabController;
|
final DesktopTabController tabController;
|
||||||
|
|
||||||
@@ -491,7 +709,6 @@ class WindowActionPanel extends StatefulWidget {
|
|||||||
const WindowActionPanel(
|
const WindowActionPanel(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required this.isMainWindow,
|
required this.isMainWindow,
|
||||||
required this.tabType,
|
|
||||||
required this.state,
|
required this.state,
|
||||||
required this.tabController,
|
required this.tabController,
|
||||||
required this.invisibleTabKeys,
|
required this.invisibleTabKeys,
|
||||||
@@ -509,180 +726,17 @@ class WindowActionPanel extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WindowActionPanelState extends State<WindowActionPanel>
|
class WindowActionPanelState extends State<WindowActionPanel> {
|
||||||
with MultiWindowListener, WindowListener {
|
|
||||||
final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
|
|
||||||
Timer? _macOSCheckRestoreTimer;
|
|
||||||
int _macOSCheckRestoreCounter = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
DesktopMultiWindow.addListener(this);
|
|
||||||
windowManager.addListener(this);
|
|
||||||
|
|
||||||
Future.delayed(Duration(milliseconds: 500), () {
|
|
||||||
if (widget.isMainWindow) {
|
|
||||||
windowManager.isMaximized().then((maximized) {
|
|
||||||
if (stateGlobal.isMaximized.value != maximized) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) => setState(() => stateGlobal.setMaximized(maximized)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
final wc = WindowController.fromWindowId(kWindowId!);
|
|
||||||
wc.isMaximized().then((maximized) {
|
|
||||||
debugPrint("isMaximized $maximized");
|
|
||||||
if (stateGlobal.isMaximized.value != maximized) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) => setState(() => stateGlobal.setMaximized(maximized)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
DesktopMultiWindow.removeListener(this);
|
|
||||||
windowManager.removeListener(this);
|
|
||||||
_macOSCheckRestoreTimer?.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setMaximized(bool maximize) {
|
|
||||||
stateGlobal.setMaximized(maximize);
|
|
||||||
_saveFrameDebounce.call(_saveFrame);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowFocus() {
|
|
||||||
stateGlobal.isFocused.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowBlur() {
|
|
||||||
stateGlobal.isFocused.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowMinimize() {
|
|
||||||
stateGlobal.setMinimized(true);
|
|
||||||
stateGlobal.setMaximized(false);
|
|
||||||
super.onWindowMinimize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowMaximize() {
|
|
||||||
stateGlobal.setMinimized(false);
|
|
||||||
_setMaximized(true);
|
|
||||||
super.onWindowMaximize();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowUnmaximize() {
|
|
||||||
stateGlobal.setMinimized(false);
|
|
||||||
_setMaximized(false);
|
|
||||||
super.onWindowUnmaximize();
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveFrame() async {
|
|
||||||
if (widget.tabType == DesktopTabType.main) {
|
|
||||||
await saveWindowPosition(WindowType.Main);
|
|
||||||
} else if (kWindowType != null && kWindowId != null) {
|
|
||||||
await saveWindowPosition(kWindowType!, windowId: kWindowId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowMoved() {
|
|
||||||
_saveFrameDebounce.call(_saveFrame);
|
|
||||||
super.onWindowMoved();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowResized() {
|
|
||||||
_saveFrameDebounce.call(_saveFrame);
|
|
||||||
super.onWindowMoved();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowClose() async {
|
|
||||||
mainWindowClose() async => await windowManager.hide();
|
|
||||||
notMainWindowClose(WindowController controller) async {
|
|
||||||
if (widget.tabController.length != 0) {
|
|
||||||
debugPrint("close not empty multiwindow from taskbar");
|
|
||||||
if (isWindows) {
|
|
||||||
await controller.show();
|
|
||||||
await controller.focus();
|
|
||||||
final res = await widget.onClose?.call() ?? true;
|
|
||||||
if (!res) return;
|
|
||||||
}
|
|
||||||
widget.tabController.clear();
|
|
||||||
}
|
|
||||||
await controller.hide();
|
|
||||||
await rustDeskWinManager
|
|
||||||
.call(WindowType.Main, kWindowEventHide, {"id": kWindowId!});
|
|
||||||
}
|
|
||||||
|
|
||||||
macOSWindowClose(
|
|
||||||
Future<bool> Function() checkFullscreen,
|
|
||||||
Future<void> Function() closeFunc,
|
|
||||||
) async {
|
|
||||||
_macOSCheckRestoreCounter = 0;
|
|
||||||
_macOSCheckRestoreTimer =
|
|
||||||
Timer.periodic(Duration(milliseconds: 30), (timer) async {
|
|
||||||
_macOSCheckRestoreCounter++;
|
|
||||||
if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
|
|
||||||
_macOSCheckRestoreTimer?.cancel();
|
|
||||||
_macOSCheckRestoreTimer = null;
|
|
||||||
Timer(Duration(milliseconds: 700), () async => await closeFunc());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide window on close
|
|
||||||
if (widget.isMainWindow) {
|
|
||||||
if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
|
|
||||||
await rustDeskWinManager.unregisterActiveWindow(kMainWindowId);
|
|
||||||
}
|
|
||||||
// macOS specific workaround, the window is not hiding when in fullscreen.
|
|
||||||
if (isMacOS && await windowManager.isFullScreen()) {
|
|
||||||
await windowManager.setFullScreen(false);
|
|
||||||
await macOSWindowClose(
|
|
||||||
() async => await windowManager.isFullScreen(),
|
|
||||||
mainWindowClose,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await mainWindowClose();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// it's safe to hide the subwindow
|
|
||||||
final controller = WindowController.fromWindowId(kWindowId!);
|
|
||||||
if (isMacOS) {
|
|
||||||
// onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart.
|
|
||||||
// use ??= to make sure the value is set on first call.
|
|
||||||
|
|
||||||
if (await widget.onClose?.call() ?? true) {
|
|
||||||
if (await controller.isFullScreen()) {
|
|
||||||
await controller.setFullscreen(false);
|
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
|
||||||
await macOSWindowClose(
|
|
||||||
() async => await controller.isFullScreen(),
|
|
||||||
() async => await notMainWindowClose(controller),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await notMainWindowClose(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await notMainWindowClose(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onWindowClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool showTabDowndown() {
|
bool showTabDowndown() {
|
||||||
return widget.tabController.state.value.tabs.length > 1 &&
|
return widget.tabController.state.value.tabs.length > 1 &&
|
||||||
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||||
@@ -703,72 +757,69 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Obx(() => Offstage(
|
Obx(() {
|
||||||
offstage:
|
if (showTabDowndown() && existingInvisibleTab().isNotEmpty) {
|
||||||
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
|
return _TabDropDownButton(
|
||||||
child: _TabDropDownButton(
|
controller: widget.tabController,
|
||||||
controller: widget.tabController,
|
labelGetter: widget.labelGetter,
|
||||||
labelGetter: widget.labelGetter,
|
tabkeys: existingInvisibleTab());
|
||||||
tabkeys: existingInvisibleTab()),
|
} else {
|
||||||
)),
|
return Offstage();
|
||||||
Offstage(offstage: widget.tail == null, child: widget.tail),
|
}
|
||||||
Offstage(
|
}),
|
||||||
offstage: kUseCompatibleUiMode,
|
if (widget.tail != null) widget.tail!,
|
||||||
child: Row(
|
if (!kUseCompatibleUiMode)
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
if (widget.showMinimize && !isMacOS)
|
||||||
offstage: !widget.showMinimize || isMacOS,
|
ActionIcon(
|
||||||
child: ActionIcon(
|
message: 'Minimize',
|
||||||
message: 'Minimize',
|
icon: IconFont.min,
|
||||||
icon: IconFont.min,
|
onTap: () {
|
||||||
onTap: () {
|
if (widget.isMainWindow) {
|
||||||
if (widget.isMainWindow) {
|
windowManager.minimize();
|
||||||
windowManager.minimize();
|
} else {
|
||||||
} else {
|
WindowController.fromWindowId(kWindowId!).minimize();
|
||||||
WindowController.fromWindowId(kWindowId!).minimize();
|
}
|
||||||
}
|
},
|
||||||
},
|
isClose: false,
|
||||||
isClose: false,
|
),
|
||||||
)),
|
if (widget.showMaximize && !isMacOS)
|
||||||
Offstage(
|
Obx(() => ActionIcon(
|
||||||
offstage: !widget.showMaximize || isMacOS,
|
message: stateGlobal.isMaximized.isTrue
|
||||||
child: Obx(() => ActionIcon(
|
? 'Restore'
|
||||||
message: stateGlobal.isMaximized.isTrue
|
: 'Maximize',
|
||||||
? 'Restore'
|
icon: stateGlobal.isMaximized.isTrue
|
||||||
: 'Maximize',
|
? IconFont.restore
|
||||||
icon: stateGlobal.isMaximized.isTrue
|
: IconFont.max,
|
||||||
? IconFont.restore
|
onTap: bind.isIncomingOnly() && isInHomePage()
|
||||||
: IconFont.max,
|
? null
|
||||||
onTap: bind.isIncomingOnly() && isInHomePage()
|
: _toggleMaximize,
|
||||||
? null
|
isClose: false,
|
||||||
: _toggleMaximize,
|
)),
|
||||||
isClose: false,
|
if (widget.showClose && !isMacOS)
|
||||||
))),
|
ActionIcon(
|
||||||
Offstage(
|
message: 'Close',
|
||||||
offstage: !widget.showClose || isMacOS,
|
icon: IconFont.close,
|
||||||
child: ActionIcon(
|
onTap: () async {
|
||||||
message: 'Close',
|
final res = await widget.onClose?.call() ?? true;
|
||||||
icon: IconFont.close,
|
if (res) {
|
||||||
onTap: () async {
|
// hide for all window
|
||||||
final res = await widget.onClose?.call() ?? true;
|
// note: the main window can be restored by tray icon
|
||||||
if (res) {
|
Future.delayed(Duration.zero, () async {
|
||||||
// hide for all window
|
if (widget.isMainWindow) {
|
||||||
// note: the main window can be restored by tray icon
|
await windowManager.close();
|
||||||
Future.delayed(Duration.zero, () async {
|
} else {
|
||||||
if (widget.isMainWindow) {
|
await WindowController.fromWindowId(kWindowId!)
|
||||||
await windowManager.close();
|
.close();
|
||||||
} else {
|
}
|
||||||
await WindowController.fromWindowId(kWindowId!)
|
});
|
||||||
.close();
|
}
|
||||||
}
|
},
|
||||||
});
|
isClose: true,
|
||||||
}
|
)
|
||||||
},
|
|
||||||
isClose: true,
|
|
||||||
))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1172,22 +1223,26 @@ class _CloseButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _kIconSize,
|
width: _kIconSize,
|
||||||
child: Offstage(
|
child: () {
|
||||||
offstage: !visible,
|
if (visible) {
|
||||||
child: InkWell(
|
return InkWell(
|
||||||
hoverColor: MyTheme.tabbar(context).closeHoverColor,
|
hoverColor: MyTheme.tabbar(context).closeHoverColor,
|
||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
onTap: () => onClose(),
|
onTap: () => onClose(),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
size: _kIconSize,
|
size: _kIconSize,
|
||||||
color: tabSelected
|
color: tabSelected
|
||||||
? MyTheme.tabbar(context).selectedIconColor
|
? MyTheme.tabbar(context).selectedIconColor
|
||||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
)).paddingOnly(left: 10);
|
} else {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
}())
|
||||||
|
.paddingOnly(left: 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1341,27 +1396,30 @@ class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
|||||||
child: InkWell(child: Text(label)),
|
child: InkWell(child: Text(label)),
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(
|
||||||
() => Offstage(
|
() {
|
||||||
offstage: !(tabInfo?.onTabCloseButton != null &&
|
if (tabInfo?.onTabCloseButton != null &&
|
||||||
menuHover.value),
|
menuHover.value) {
|
||||||
child: InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
tabInfo?.onTabCloseButton?.call();
|
tabInfo?.onTabCloseButton?.call();
|
||||||
if (Navigator.of(context).canPop()) {
|
if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
onHover: (event) =>
|
onHover: (event) =>
|
||||||
setState(() => btnHover.value = true),
|
setState(() => btnHover.value = true),
|
||||||
onExit: (event) =>
|
onExit: (event) =>
|
||||||
setState(() => btnHover.value = false),
|
setState(() => btnHover.value = false),
|
||||||
child: Icon(Icons.close,
|
child: Icon(Icons.close,
|
||||||
color:
|
color:
|
||||||
btnHover.value ? Colors.red : null))),
|
btnHover.value ? Colors.red : null)));
|
||||||
),
|
} else {
|
||||||
)
|
return Offstage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ class _WebMenuState extends State<WebMenu> {
|
|||||||
[
|
[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: "about",
|
value: "about",
|
||||||
child: Text('${translate('About')} RustDesk'),
|
child: Text(translate('About RustDesk')),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
InputModel get inputModel => gFFI.inputModel;
|
InputModel get inputModel => gFFI.inputModel;
|
||||||
SessionID get sessionId => gFFI.sessionId;
|
SessionID get sessionId => gFFI.sessionId;
|
||||||
|
|
||||||
|
final TextEditingController _textController =
|
||||||
|
TextEditingController(text: initText);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -145,37 +148,59 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle mobile virtual keyboard
|
void _handleIOSSoftKeyboardInput(String newValue) {
|
||||||
void handleSoftKeyboardInput(String newValue) {
|
|
||||||
var oldValue = _value;
|
var oldValue = _value;
|
||||||
_value = newValue;
|
_value = newValue;
|
||||||
if (isIOS) {
|
var i = newValue.length - 1;
|
||||||
var i = newValue.length - 1;
|
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
||||||
for (; i >= 0 && newValue[i] != '\1'; --i) {}
|
var j = oldValue.length - 1;
|
||||||
var j = oldValue.length - 1;
|
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
||||||
for (; j >= 0 && oldValue[j] != '\1'; --j) {}
|
if (i < j) j = i;
|
||||||
if (i < j) j = i;
|
var subNewValue = newValue.substring(j + 1);
|
||||||
newValue = newValue.substring(j + 1);
|
var subOldValue = oldValue.substring(j + 1);
|
||||||
oldValue = oldValue.substring(j + 1);
|
|
||||||
var common = 0;
|
// get common prefix of subNewValue and subOldValue
|
||||||
for (;
|
var common = 0;
|
||||||
common < oldValue.length &&
|
for (;
|
||||||
common < newValue.length &&
|
common < subOldValue.length &&
|
||||||
newValue[common] == oldValue[common];
|
common < subNewValue.length &&
|
||||||
++common) {}
|
subNewValue[common] == subOldValue[common];
|
||||||
for (i = 0; i < oldValue.length - common; ++i) {
|
++common) {}
|
||||||
inputModel.inputKey('VK_BACK');
|
|
||||||
}
|
// get newStr from subNewValue
|
||||||
if (newValue.length > common) {
|
var newStr = "";
|
||||||
var s = newValue.substring(common);
|
if (subNewValue.length > common) {
|
||||||
if (s.length > 1) {
|
newStr = subNewValue.substring(common);
|
||||||
bind.sessionInputString(sessionId: sessionId, value: s);
|
|
||||||
} else {
|
|
||||||
inputChar(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the value to the old value and early return if is still composing. (1 && 2)
|
||||||
|
// 1. The composing range is valid
|
||||||
|
// 2. The new string is shorter than the composing range.
|
||||||
|
if (_textController.value.isComposingRangeValid) {
|
||||||
|
final composingLength = _textController.value.composing.end -
|
||||||
|
_textController.value.composing.start;
|
||||||
|
if (composingLength > newStr.length) {
|
||||||
|
_value = oldValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the different part in the old value.
|
||||||
|
for (i = 0; i < subOldValue.length - common; ++i) {
|
||||||
|
inputModel.inputKey('VK_BACK');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input the new string.
|
||||||
|
if (newStr.length > 1) {
|
||||||
|
bind.sessionInputString(sessionId: sessionId, value: newStr);
|
||||||
|
} else {
|
||||||
|
inputChar(newStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleNonIOSSoftKeyboardInput(String newValue) {
|
||||||
|
var oldValue = _value;
|
||||||
|
_value = newValue;
|
||||||
if (oldValue.isNotEmpty &&
|
if (oldValue.isNotEmpty &&
|
||||||
newValue.isNotEmpty &&
|
newValue.isNotEmpty &&
|
||||||
oldValue[0] == '\1' &&
|
oldValue[0] == '\1' &&
|
||||||
@@ -214,6 +239,15 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle mobile virtual keyboard
|
||||||
|
void handleSoftKeyboardInput(String newValue) {
|
||||||
|
if (isIOS) {
|
||||||
|
_handleIOSSoftKeyboardInput(newValue);
|
||||||
|
} else {
|
||||||
|
_handleNonIOSSoftKeyboardInput(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void inputChar(String char) {
|
void inputChar(String char) {
|
||||||
if (char == '\n') {
|
if (char == '\n') {
|
||||||
char = 'VK_RETURN';
|
char = 'VK_RETURN';
|
||||||
@@ -227,6 +261,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
// destroy first, so that our _value trick can work
|
// destroy first, so that our _value trick can work
|
||||||
_value = initText;
|
_value = initText;
|
||||||
|
_textController.text = _value;
|
||||||
setState(() => _showEdit = false);
|
setState(() => _showEdit = false);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
||||||
@@ -242,12 +277,10 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
|
|
||||||
|
|
||||||
Widget _bottomWidget() => _showGestureHelp
|
Widget _bottomWidget() => _showGestureHelp
|
||||||
? getGestureHelp()
|
? getGestureHelp()
|
||||||
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
|
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
|
||||||
? getBottomAppBar(keyboard)
|
? getBottomAppBar()
|
||||||
: Offstage());
|
: Offstage());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -314,7 +347,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
return Container(
|
return Container(
|
||||||
color: kColorCanvas,
|
color: kColorCanvas,
|
||||||
child: isWebDesktop
|
child: isWebDesktop
|
||||||
? getBodyForDesktopWithListener(keyboard)
|
? getBodyForDesktopWithListener()
|
||||||
: SafeArea(
|
: SafeArea(
|
||||||
child:
|
child:
|
||||||
OrientationBuilder(builder: (ctx, orientation) {
|
OrientationBuilder(builder: (ctx, orientation) {
|
||||||
@@ -346,9 +379,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget getRawPointerAndKeyBody(Widget child) {
|
Widget getRawPointerAndKeyBody(Widget child) {
|
||||||
final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
return RawPointerMouseRegion(
|
return RawPointerMouseRegion(
|
||||||
cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
cursor: ffiModel.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
|
||||||
inputModel: inputModel,
|
inputModel: inputModel,
|
||||||
// Disable RawKeyFocusScope before the connecting is established.
|
// Disable RawKeyFocusScope before the connecting is established.
|
||||||
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
|
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
|
||||||
@@ -361,7 +394,8 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getBottomAppBar(bool keyboard) {
|
Widget getBottomAppBar() {
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
return BottomAppBar(
|
return BottomAppBar(
|
||||||
elevation: 10,
|
elevation: 10,
|
||||||
color: MyTheme.accent,
|
color: MyTheme.accent,
|
||||||
@@ -387,7 +421,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
] +
|
] +
|
||||||
(isWebDesktop
|
(isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
|
||||||
? []
|
? []
|
||||||
: gFFI.ffiModel.isPeerAndroid
|
: gFFI.ffiModel.isPeerAndroid
|
||||||
? [
|
? [
|
||||||
@@ -491,7 +525,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
focusNode: _mobileFocusNode,
|
focusNode: _mobileFocusNode,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
initialValue: _value,
|
controller: _textController,
|
||||||
// trick way to make backspace work always
|
// trick way to make backspace work always
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
onChanged: handleSoftKeyboardInput,
|
onChanged: handleSoftKeyboardInput,
|
||||||
@@ -499,48 +533,84 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (showCursorPaint) {
|
if (showCursorPaint) {
|
||||||
paints.add(CursorPaint());
|
paints.add(CursorPaint(widget.id));
|
||||||
}
|
}
|
||||||
return paints;
|
return paints;
|
||||||
}()));
|
}()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getBodyForDesktopWithListener(bool keyboard) {
|
Widget getBodyForDesktopWithListener() {
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
var paints = <Widget>[ImagePaint()];
|
var paints = <Widget>[ImagePaint()];
|
||||||
if (showCursorPaint) {
|
if (showCursorPaint) {
|
||||||
final cursor = bind.sessionGetToggleOptionSync(
|
final cursor = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
if (keyboard || cursor) {
|
if (ffiModel.keyboard || cursor) {
|
||||||
paints.add(CursorPaint());
|
paints.add(CursorPaint(widget.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor, child: Stack(children: paints));
|
color: MyTheme.canvasColor, child: Stack(children: paints));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<TTextMenu> _getMobileActionMenus() {
|
||||||
|
if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
|
||||||
|
!gFFI.ffiModel.keyboard) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
|
||||||
|
if (!enabled) return [];
|
||||||
|
return [
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Back')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileBack(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Home')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileHome(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Apps')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileApps(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Volume up')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Volume down')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
|
||||||
|
),
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate('Power')),
|
||||||
|
onPressed: () => gFFI.inputModel.onMobilePower(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void showActions(String id) async {
|
void showActions(String id) async {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final x = 120.0;
|
final x = 120.0;
|
||||||
final y = size.height;
|
final y = size.height;
|
||||||
|
final mobileActionMenus = _getMobileActionMenus();
|
||||||
final menus = toolbarControls(context, id, gFFI);
|
final menus = toolbarControls(context, id, gFFI);
|
||||||
getChild(TTextMenu menu) {
|
|
||||||
if (menu.trailingIcon != null) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
menu.child,
|
|
||||||
menu.trailingIcon!,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return menu.child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final more = menus
|
final List<PopupMenuEntry<int>> more = [
|
||||||
.asMap()
|
...mobileActionMenus
|
||||||
.entries
|
.asMap()
|
||||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
.entries
|
||||||
.toList();
|
.map((e) =>
|
||||||
|
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
|
||||||
|
.toList(),
|
||||||
|
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
|
||||||
|
...menus
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => PopupMenuItem<int>(
|
||||||
|
child: e.value.getChild(),
|
||||||
|
value: e.key + mobileActionMenus.length))
|
||||||
|
.toList(),
|
||||||
|
];
|
||||||
() async {
|
() async {
|
||||||
var index = await showMenu(
|
var index = await showMenu(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -548,8 +618,12 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
items: more,
|
items: more,
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
);
|
);
|
||||||
if (index != null && index < menus.length) {
|
if (index != null) {
|
||||||
menus[index].onPressed.call();
|
if (index < mobileActionMenus.length) {
|
||||||
|
mobileActionMenus[index].onPressed.call();
|
||||||
|
} else if (index < mobileActionMenus.length + more.length) {
|
||||||
|
menus[index - mobileActionMenus.length].onPressed.call();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
@@ -569,9 +643,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
child: Text(translate(label), style: labelStyle),
|
child: Text(translate(label), style: labelStyle),
|
||||||
trailingIcon: Transform.scale(
|
trailingIcon: Transform.scale(
|
||||||
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||||
child: IconButton(
|
child: IgnorePointer(
|
||||||
onPressed: onPressed,
|
child: IconButton(
|
||||||
icon: icon,
|
onPressed: null,
|
||||||
|
icon: icon,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
@@ -602,23 +678,11 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
),
|
),
|
||||||
onPressVoiceCall),
|
onPressVoiceCall),
|
||||||
];
|
];
|
||||||
getChild(TTextMenu menu) {
|
|
||||||
if (menu.trailingIcon != null) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
menu.child,
|
|
||||||
menu.trailingIcon!,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return menu.child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final menuItems = menus
|
final menuItems = menus
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
.map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
|
||||||
.toList();
|
.toList();
|
||||||
Future.delayed(Duration.zero, () async {
|
Future.delayed(Duration.zero, () async {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -885,26 +949,52 @@ class ImagePaint extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CursorPaint extends StatelessWidget {
|
class CursorPaint extends StatelessWidget {
|
||||||
|
late final String id;
|
||||||
|
CursorPaint(this.id);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final m = Provider.of<CursorModel>(context);
|
final m = Provider.of<CursorModel>(context);
|
||||||
final c = Provider.of<CanvasModel>(context);
|
final c = Provider.of<CanvasModel>(context);
|
||||||
|
final ffiModel = Provider.of<FfiModel>(context);
|
||||||
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
final adjust = gFFI.cursorModel.adjustForKeyboard();
|
||||||
var s = c.scale;
|
final s = c.scale;
|
||||||
double hotx = m.hotx;
|
double hotx = m.hotx;
|
||||||
double hoty = m.hoty;
|
double hoty = m.hoty;
|
||||||
if (m.image == null) {
|
var image = m.image;
|
||||||
|
if (image == null) {
|
||||||
if (preDefaultCursor.image != null) {
|
if (preDefaultCursor.image != null) {
|
||||||
|
image = preDefaultCursor.image;
|
||||||
hotx = preDefaultCursor.image!.width / 2;
|
hotx = preDefaultCursor.image!.width / 2;
|
||||||
hoty = preDefaultCursor.image!.height / 2;
|
hoty = preDefaultCursor.image!.height / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (preForbiddenCursor.image != null &&
|
||||||
|
!ffiModel.viewOnly &&
|
||||||
|
!ffiModel.keyboard &&
|
||||||
|
!ShowRemoteCursorState.find(id).value) {
|
||||||
|
image = preForbiddenCursor.image;
|
||||||
|
hotx = preForbiddenCursor.image!.width / 2;
|
||||||
|
hoty = preForbiddenCursor.image!.height / 2;
|
||||||
|
}
|
||||||
|
if (image == null) {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final minSize = 12.0;
|
||||||
|
double mins =
|
||||||
|
minSize / (image.width > image.height ? image.width : image.height);
|
||||||
|
double factor = 1.0;
|
||||||
|
if (s < mins) {
|
||||||
|
factor = s / mins;
|
||||||
|
}
|
||||||
|
final s2 = s < mins ? mins : s;
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: ImagePainter(
|
painter: ImagePainter(
|
||||||
image: m.image ?? preDefaultCursor.image,
|
image: image,
|
||||||
x: m.x * s - hotx + c.x,
|
x: (m.x - hotx) * factor + c.x / s2,
|
||||||
y: m.y * s - hoty + c.y - adjust,
|
y: (m.y - hoty) * factor + (c.y - adjust) / s2,
|
||||||
scale: 1),
|
scale: s2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -982,22 +1072,40 @@ void showOptions(
|
|||||||
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
|
||||||
final radios = [
|
final radios = [
|
||||||
for (var e in viewStyleRadios)
|
for (var e in viewStyleRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, viewStyle.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
e.onChanged?.call(v);
|
e.child,
|
||||||
if (v != null) viewStyle.value = v;
|
e.value,
|
||||||
})),
|
viewStyle.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
|
e.onChanged?.call(v);
|
||||||
|
if (v != null) viewStyle.value = v;
|
||||||
|
}
|
||||||
|
: null)),
|
||||||
const Divider(color: MyTheme.border),
|
const Divider(color: MyTheme.border),
|
||||||
for (var e in imageQualityRadios)
|
for (var e in imageQualityRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, imageQuality.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
e.onChanged?.call(v);
|
e.child,
|
||||||
if (v != null) imageQuality.value = v;
|
e.value,
|
||||||
})),
|
imageQuality.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
|
e.onChanged?.call(v);
|
||||||
|
if (v != null) imageQuality.value = v;
|
||||||
|
}
|
||||||
|
: null)),
|
||||||
const Divider(color: MyTheme.border),
|
const Divider(color: MyTheme.border),
|
||||||
for (var e in codecRadios)
|
for (var e in codecRadios)
|
||||||
Obx(() => getRadio<String>(e.child, e.value, codec.value, (v) {
|
Obx(() => getRadio<String>(
|
||||||
e.onChanged?.call(v);
|
e.child,
|
||||||
if (v != null) codec.value = v;
|
e.value,
|
||||||
})),
|
codec.value,
|
||||||
|
e.onChanged != null
|
||||||
|
? (v) {
|
||||||
|
e.onChanged?.call(v);
|
||||||
|
if (v != null) codec.value = v;
|
||||||
|
}
|
||||||
|
: null)),
|
||||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||||
];
|
];
|
||||||
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
||||||
@@ -1008,10 +1116,12 @@ void showOptions(
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
value: rxCursorToggleValues[e.key].value,
|
value: rxCursorToggleValues[e.key].value,
|
||||||
onChanged: (v) {
|
onChanged: e.value.onChanged != null
|
||||||
e.value.onChanged?.call(v);
|
? (v) {
|
||||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
e.value.onChanged?.call(v);
|
||||||
},
|
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||||
|
}
|
||||||
|
: null,
|
||||||
title: e.value.child)))
|
title: e.value.child)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -1023,10 +1133,12 @@ void showOptions(
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
value: rxToggleValues[e.key].value,
|
value: rxToggleValues[e.key].value,
|
||||||
onChanged: (v) {
|
onChanged: e.value.onChanged != null
|
||||||
e.value.onChanged?.call(v);
|
? (v) {
|
||||||
if (v != null) rxToggleValues[e.key].value = v;
|
e.value.onChanged?.call(v);
|
||||||
},
|
if (v != null) rxToggleValues[e.key].value = v;
|
||||||
|
}
|
||||||
|
: null,
|
||||||
title: e.value.child)))
|
title: e.value.child)))
|
||||||
.toList();
|
.toList();
|
||||||
final toggles = [
|
final toggles = [
|
||||||
@@ -1046,14 +1158,110 @@ void showOptions(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var popupDialogMenus = List<Widget>.empty(growable: true);
|
||||||
|
final resolution = getResolutionMenu(gFFI, id);
|
||||||
|
if (resolution != null) {
|
||||||
|
popupDialogMenus.add(ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: resolution.child,
|
||||||
|
onTap: () {
|
||||||
|
close();
|
||||||
|
resolution.onPressed();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
final virtualDisplayMenu = getVirtualDisplayMenu(gFFI, id);
|
||||||
|
if (virtualDisplayMenu != null) {
|
||||||
|
popupDialogMenus.add(ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: virtualDisplayMenu.child,
|
||||||
|
onTap: () {
|
||||||
|
close();
|
||||||
|
virtualDisplayMenu.onPressed();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (popupDialogMenus.isNotEmpty) {
|
||||||
|
popupDialogMenus.add(const Divider(color: MyTheme.border));
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: displays + radios + toggles + [privacyModeWidget]),
|
children: displays +
|
||||||
|
radios +
|
||||||
|
popupDialogMenus +
|
||||||
|
toggles +
|
||||||
|
[privacyModeWidget]),
|
||||||
);
|
);
|
||||||
}, clickMaskDismiss: true, backDismiss: true);
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
|
||||||
|
if (!showVirtualDisplayMenu(ffi)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return TTextMenu(
|
||||||
|
child: Text(translate("Virtual display")),
|
||||||
|
onPressed: () {
|
||||||
|
ffi.dialogManager.show((setState, close, context) {
|
||||||
|
final children = getVirtualDisplayMenuChildren(ffi, id, close);
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('Virtual display')),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TTextMenu? getResolutionMenu(FFI ffi, String id) {
|
||||||
|
final ffiModel = ffi.ffiModel;
|
||||||
|
final pi = ffiModel.pi;
|
||||||
|
final resolutions = pi.resolutions;
|
||||||
|
final display = pi.tryGetDisplayIfNotAllDisplay(display: pi.currentDisplay);
|
||||||
|
|
||||||
|
final visible =
|
||||||
|
ffiModel.keyboard && (resolutions.length > 1) && display != null;
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return TTextMenu(
|
||||||
|
child: Text(translate("Resolution")),
|
||||||
|
onPressed: () {
|
||||||
|
ffi.dialogManager.show((setState, close, context) {
|
||||||
|
final children = resolutions
|
||||||
|
.map((e) => getRadio<String>(
|
||||||
|
Text('${e.width}x${e.height}'),
|
||||||
|
'${e.width}x${e.height}',
|
||||||
|
'${display.width}x${display.height}',
|
||||||
|
(value) {
|
||||||
|
close();
|
||||||
|
bind.sessionChangeResolution(
|
||||||
|
sessionId: ffi.sessionId,
|
||||||
|
display: pi.currentDisplay,
|
||||||
|
width: e.width,
|
||||||
|
height: e.height,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('Resolution')),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, clickMaskDismiss: true, backDismiss: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void sendPrompt(bool isMac, String key) {
|
void sendPrompt(bool isMac, String key) {
|
||||||
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||||
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -22,7 +23,22 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
final icon = const Icon(Icons.mobile_screen_share);
|
final icon = const Icon(Icons.mobile_screen_share);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final appBarActions = [
|
final appBarActions = (!bind.isDisableSettings() &&
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
|
||||||
|
? [_DropDownAction()]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
ServerPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ServerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DropDownAction extends StatelessWidget {
|
||||||
|
_DropDownAction();
|
||||||
|
|
||||||
|
// should only have one action
|
||||||
|
final actions = [
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
tooltip: "",
|
tooltip: "",
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
@@ -101,18 +117,27 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onSelected: (value) {
|
onSelected: (value) async {
|
||||||
if (value == "changeID") {
|
if (value == "changeID") {
|
||||||
changeIdDialog();
|
changeIdDialog();
|
||||||
} else if (value == "setPermanentPassword") {
|
} else if (value == "setPermanentPassword") {
|
||||||
setPermanentPasswordDialog(gFFI.dialogManager);
|
setPasswordDialog();
|
||||||
} else if (value == "setTemporaryPasswordLength") {
|
} else if (value == "setTemporaryPasswordLength") {
|
||||||
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
setTemporaryPasswordLengthDialog(gFFI.dialogManager);
|
||||||
} else if (value == kUsePermanentPassword ||
|
} else if (value == kUsePermanentPassword ||
|
||||||
value == kUseTemporaryPassword ||
|
value == kUseTemporaryPassword ||
|
||||||
value == kUseBothPasswords) {
|
value == kUseBothPasswords) {
|
||||||
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
callback() {
|
||||||
gFFI.serverModel.updatePasswordModel();
|
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
||||||
|
gFFI.serverModel.updatePasswordModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == kUsePermanentPassword &&
|
||||||
|
(await bind.mainGetPermanentPassword()).isEmpty) {
|
||||||
|
setPasswordDialog(notEmptyCallback: callback);
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
} else if (value.startsWith("AcceptSessionsVia")) {
|
} else if (value.startsWith("AcceptSessionsVia")) {
|
||||||
value = value.substring("AcceptSessionsVia".length);
|
value = value.substring("AcceptSessionsVia".length);
|
||||||
if (value == "Password") {
|
if (value == "Password") {
|
||||||
@@ -126,10 +151,10 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
ServerPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _ServerPageState();
|
Widget build(BuildContext context) {
|
||||||
|
return actions[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerPageState extends State<ServerPage> {
|
class _ServerPageState extends State<ServerPage> {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
import 'package:settings_ui/settings_ui.dart';
|
||||||
@@ -83,6 +84,9 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _fingerprint = "";
|
var _fingerprint = "";
|
||||||
var _buildDate = "";
|
var _buildDate = "";
|
||||||
var _autoDisconnectTimeout = "";
|
var _autoDisconnectTimeout = "";
|
||||||
|
var _hideServer = false;
|
||||||
|
var _hideProxy = false;
|
||||||
|
var _hideNetwork = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -93,8 +97,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
||||||
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
||||||
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
|
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
|
||||||
_onlyWhiteList = (bind.mainGetOptionSync(key: kOptionWhitelist)) !=
|
_onlyWhiteList = whitelistNotEmpty();
|
||||||
defaultOptionWhitelist;
|
|
||||||
_enableDirectIPAccess = option2bool(
|
_enableDirectIPAccess = option2bool(
|
||||||
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
|
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
|
||||||
_enableRecordSession = option2bool(kOptionEnableRecordSession,
|
_enableRecordSession = option2bool(kOptionEnableRecordSession,
|
||||||
@@ -109,6 +112,11 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
|
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
|
||||||
_autoDisconnectTimeout =
|
_autoDisconnectTimeout =
|
||||||
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
|
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
|
||||||
|
_hideServer =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
||||||
|
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
||||||
|
_hideNetwork =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
||||||
|
|
||||||
() async {
|
() async {
|
||||||
var update = false;
|
var update = false;
|
||||||
@@ -273,9 +281,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
initialValue: _onlyWhiteList,
|
initialValue: _onlyWhiteList,
|
||||||
onToggle: (_) async {
|
onToggle: (_) async {
|
||||||
update() async {
|
update() async {
|
||||||
final onlyWhiteList =
|
final onlyWhiteList = whitelistNotEmpty();
|
||||||
(await bind.mainGetOption(key: kOptionWhitelist)) !=
|
|
||||||
defaultOptionWhitelist;
|
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
if (onlyWhiteList != _onlyWhiteList) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onlyWhiteList = onlyWhiteList;
|
_onlyWhiteList = onlyWhiteList;
|
||||||
@@ -530,6 +536,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
));
|
));
|
||||||
|
|
||||||
final disabledSettings = bind.isDisableSettings();
|
final disabledSettings = bind.isDisableSettings();
|
||||||
|
final hideSecuritySettings =
|
||||||
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
|
||||||
final settings = SettingsList(
|
final settings = SettingsList(
|
||||||
sections: [
|
sections: [
|
||||||
customClientSection,
|
customClientSection,
|
||||||
@@ -553,13 +561,20 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||||
if (!disabledSettings)
|
if (!disabledSettings && !_hideNetwork && !_hideServer)
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate('ID/Relay Server')),
|
title: Text(translate('ID/Relay Server')),
|
||||||
leading: Icon(Icons.cloud),
|
leading: Icon(Icons.cloud),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
showServerSettings(gFFI.dialogManager);
|
showServerSettings(gFFI.dialogManager);
|
||||||
}),
|
}),
|
||||||
|
if (!isIOS && !_hideNetwork && !_hideProxy)
|
||||||
|
SettingsTile(
|
||||||
|
title: Text(translate('Socks5/Http(s) Proxy')),
|
||||||
|
leading: Icon(Icons.network_ping),
|
||||||
|
onPressed: (context) {
|
||||||
|
changeSocks5Proxy();
|
||||||
|
}),
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate('Language')),
|
title: Text(translate('Language')),
|
||||||
leading: Icon(Icons.translate),
|
leading: Icon(Icons.translate),
|
||||||
@@ -625,13 +640,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
if (isAndroid &&
|
||||||
|
!disabledSettings &&
|
||||||
|
!outgoingOnly &&
|
||||||
|
!hideSecuritySettings)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Share Screen")),
|
title: Text(translate("Share Screen")),
|
||||||
tiles: shareScreenTiles,
|
tiles: shareScreenTiles,
|
||||||
),
|
),
|
||||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||||
if (isAndroid && !disabledSettings && !outgoingOnly)
|
if (isAndroid &&
|
||||||
|
!disabledSettings &&
|
||||||
|
!outgoingOnly &&
|
||||||
|
!hideSecuritySettings)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Enhancements")),
|
title: Text(translate("Enhancements")),
|
||||||
tiles: enhancementsTiles,
|
tiles: enhancementsTiles,
|
||||||
@@ -786,7 +807,7 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
|||||||
void showAbout(OverlayDialogManager dialogManager) {
|
void showAbout(OverlayDialogManager dialogManager) {
|
||||||
dialogManager.show((setState, close, context) {
|
dialogManager.show((setState, close, context) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text('${translate('About')} RustDesk'),
|
title: Text(translate('About RustDesk')),
|
||||||
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
||||||
Text('Version: $version'),
|
Text('Version: $version'),
|
||||||
InkWell(
|
InkWell(
|
||||||
|
|||||||
@@ -1152,4 +1152,27 @@ class InputModel {
|
|||||||
platformFFI.stopDesktopWebListener();
|
platformFFI.stopDesktopWebListener();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onMobileBack() => tap(MouseButtons.right);
|
||||||
|
void onMobileHome() => tap(MouseButtons.wheel);
|
||||||
|
Future<void> onMobileApps() async {
|
||||||
|
sendMouse('down', MouseButtons.wheel);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
sendMouse('up', MouseButtons.wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate a key press event.
|
||||||
|
// `usbHidUsage` is the USB HID usage code of the key.
|
||||||
|
Future<void> tapHidKey(int usbHidUsage) async {
|
||||||
|
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true);
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onMobileVolumeUp() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage);
|
||||||
|
Future<void> onMobileVolumeDown() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage);
|
||||||
|
Future<void> onMobilePower() async =>
|
||||||
|
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,10 +192,10 @@ class FfiModel with ChangeNotifier {
|
|||||||
_permissions[k] = v == 'true';
|
_permissions[k] = v == 'true';
|
||||||
});
|
});
|
||||||
// Only inited at remote page
|
// Only inited at remote page
|
||||||
if (desktopType == DesktopType.remote) {
|
if (parent.target?.connType == ConnType.defaultConn) {
|
||||||
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||||
}
|
}
|
||||||
debugPrint('$_permissions');
|
debugPrint('updatePermission: $_permissions');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,20 +438,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
|
_handlePortableServiceRunning(String peerId, Map<String, dynamic> evt) {
|
||||||
final running = evt['running'] == 'true';
|
final running = evt['running'] == 'true';
|
||||||
parent.target?.elevationModel.onPortableServiceRunning(running);
|
parent.target?.elevationModel.onPortableServiceRunning(running);
|
||||||
if (running) {
|
|
||||||
if (pi.primaryDisplay != kInvalidDisplayIndex) {
|
|
||||||
if (pi.currentDisplay != pi.primaryDisplay) {
|
|
||||||
// Notify to switch display
|
|
||||||
msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt',
|
|
||||||
'elevated_switch_display_msg', '', parent.target!.dialogManager);
|
|
||||||
bind.sessionSwitchDisplay(
|
|
||||||
isDesktop: isDesktop,
|
|
||||||
sessionId: sessionId,
|
|
||||||
value: Int32List.fromList([pi.primaryDisplay]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAliasChanged(Map<String, dynamic> evt) {
|
handleAliasChanged(Map<String, dynamic> evt) {
|
||||||
@@ -1740,7 +1726,7 @@ class PredefinedCursor {
|
|||||||
_image2 = img2.decodePng(base64Decode(png));
|
_image2 = img2.decodePng(base64Decode(png));
|
||||||
if (_image2 != null) {
|
if (_image2 != null) {
|
||||||
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
||||||
if (isWindows && id == kPreForbiddenCursorId) {
|
if (id == kPreForbiddenCursorId) {
|
||||||
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
|
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,9 +117,13 @@ class PlatformFFI {
|
|||||||
? DynamicLibrary.open('librustdesk.so')
|
? DynamicLibrary.open('librustdesk.so')
|
||||||
: isWindows
|
: isWindows
|
||||||
? DynamicLibrary.open('librustdesk.dll')
|
? DynamicLibrary.open('librustdesk.dll')
|
||||||
: isMacOS
|
:
|
||||||
? DynamicLibrary.open("liblibrustdesk.dylib")
|
// Use executable itself as the dynamic library for MacOS.
|
||||||
: DynamicLibrary.process();
|
// Multiple dylib instances will cause some global instances to be invalid.
|
||||||
|
// eg. `lazy_static` objects in rust side, will be created more than once, which is not expected.
|
||||||
|
//
|
||||||
|
// isMacOS? DynamicLibrary.open("liblibrustdesk.dylib") :
|
||||||
|
DynamicLibrary.process();
|
||||||
debugPrint('initializing FFI $_appType');
|
debugPrint('initializing FFI $_appType');
|
||||||
try {
|
try {
|
||||||
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
|
_session_get_rgba = dylib.lookupFunction<F3Dart, F3>("session_get_rgba");
|
||||||
|
|||||||
@@ -177,6 +177,11 @@ class ServerModel with ChangeNotifier {
|
|||||||
await timerCallback();
|
await timerCallback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial keyboard status is off on mobile
|
||||||
|
if (isMobile) {
|
||||||
|
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 1. check android permission
|
/// 1. check android permission
|
||||||
|
|||||||
@@ -1614,5 +1614,13 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool mainHasValidBotSync({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> mainVerifyBot({required String token, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: "60773827434eefe6d01eefa814dca9a032b970b3"
|
resolved-ref: "336308d86ec8b9640504a371b50ba500eb779363"
|
||||||
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.2.6+44
|
version: 1.2.7+46
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
@@ -79,7 +79,7 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
url: https://github.com/rustdesk-org/flutter_improved_scrolling
|
||||||
uni_links: ^0.5.1
|
uni_links: ^0.5.1
|
||||||
uni_links_desktop: ^0.1.7
|
uni_links_desktop: ^0.1.6 # use 0.1.6 to make flutter 3.13 works
|
||||||
path: ^1.8.1
|
path: ^1.8.1
|
||||||
auto_size_text: ^3.0.0
|
auto_size_text: ^3.0.0
|
||||||
bot_toast: ^4.0.3
|
bot_toast: ^4.0.3
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
#include "resource.h"
|
#include "resource.h"
|
||||||
|
|
||||||
|
#include <cstdlib> // for getenv and _putenv
|
||||||
|
#include <cstring> // for strcmp
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||||
@@ -143,6 +146,25 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
|
|||||||
return OnCreate();
|
return OnCreate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void trySetWindowForeground(HWND window) {
|
||||||
|
char* value = nullptr;
|
||||||
|
size_t size = 0;
|
||||||
|
// Use _dupenv_s to safely get the environment variable
|
||||||
|
_dupenv_s(&value, &size, "SET_FOREGROUND_WINDOW");
|
||||||
|
|
||||||
|
if (value != nullptr) {
|
||||||
|
// Correctly compare the value with "1"
|
||||||
|
if (strcmp(value, "1") == 0) {
|
||||||
|
// Clear the environment variable
|
||||||
|
_putenv("SET_FOREGROUND_WINDOW=");
|
||||||
|
// Set the window to foreground
|
||||||
|
SetForegroundWindow(window);
|
||||||
|
}
|
||||||
|
// Free the duplicated string
|
||||||
|
free(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||||
UINT const message,
|
UINT const message,
|
||||||
@@ -156,6 +178,7 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
|||||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||||
EnableFullDpiSupportIfAvailable(window);
|
EnableFullDpiSupportIfAvailable(window);
|
||||||
that->window_handle_ = window;
|
that->window_handle_ = window;
|
||||||
|
trySetWindowForeground(window);
|
||||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||||
return that->MessageHandler(window, message, wparam, lparam);
|
return that->MessageHandler(window, message, wparam, lparam);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -795,7 +795,11 @@ impl FuseNode {
|
|||||||
conn_id: desc.conn_id,
|
conn_id: desc.conn_id,
|
||||||
stream_id: rand::random(),
|
stream_id: rand::random(),
|
||||||
index: inode as usize - 2,
|
index: inode as usize - 2,
|
||||||
name: desc.name.to_str().unwrap().to_owned(),
|
name: desc
|
||||||
|
.name
|
||||||
|
.to_str()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
parent: None,
|
parent: None,
|
||||||
attributes: InodeAttributes::from_description(inode, desc),
|
attributes: InodeAttributes::from_description(inode, desc),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -1140,7 +1144,7 @@ mod fuse_test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_single_file(prefix: &str) {
|
fn build_single_file(prefix: &str) {
|
||||||
let raw_name = "衬衫的价格为 9 镑 15 便士.txt";
|
let raw_name = "simple_test_file.txt";
|
||||||
let f_name = if prefix == "" {
|
let f_name = if prefix == "" {
|
||||||
raw_name.to_string()
|
raw_name.to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ pub fn create_cliprdr_context(
|
|||||||
log::warn!("umount {:?} may fail: {:?}", mnt_path, e);
|
log::warn!("umount {:?} may fail: {:?}", mnt_path, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse().unwrap())?;
|
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?;
|
||||||
log::debug!("start cliprdr FUSE");
|
log::debug!("start cliprdr FUSE");
|
||||||
unix_ctx.run().expect("failed to start cliprdr FUSE");
|
unix_ctx.run()?;
|
||||||
|
|
||||||
Ok(Box::new(unix_ctx) as Box<_>)
|
Ok(Box::new(unix_ctx) as Box<_>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ impl LocalFile {
|
|||||||
let win32_time = self
|
let win32_time = self
|
||||||
.last_write_time
|
.last_write_time
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap_or_default()
|
||||||
.as_nanos() as u64
|
.as_nanos() as u64
|
||||||
/ 100
|
/ 100
|
||||||
+ LDAP_EPOCH_DELTA;
|
+ LDAP_EPOCH_DELTA;
|
||||||
@@ -188,7 +188,7 @@ impl LocalFile {
|
|||||||
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
|
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
|
||||||
self.load_handle()?;
|
self.load_handle()?;
|
||||||
|
|
||||||
let handle = self.handle.as_mut().unwrap();
|
let handle = self.handle.as_mut()?;
|
||||||
|
|
||||||
if offset != self.offset.load(Ordering::Relaxed) {
|
if offset != self.offset.load(Ordering::Relaxed) {
|
||||||
handle
|
handle
|
||||||
@@ -238,9 +238,9 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if mt.is_dir() {
|
if mt.is_dir() {
|
||||||
let dir = std::fs::read_dir(path).unwrap();
|
let dir = std::fs::read_dir(path)?;
|
||||||
for entry in dir {
|
for entry in dir {
|
||||||
let entry = entry.unwrap();
|
let entry = entry?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
constr_file_lst(&path, file_list, visited)?;
|
constr_file_lst(&path, file_list, visited)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,13 +383,11 @@ impl ClipboardContext {
|
|||||||
let file_contents_id = fmt_lst
|
let file_contents_id = fmt_lst
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
|
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
|
||||||
.map(|(id, _)| *id)
|
.map(|(id, _)| *id)?;
|
||||||
.unwrap();
|
|
||||||
let file_descriptor_id = fmt_lst
|
let file_descriptor_id = fmt_lst
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
|
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
|
||||||
.map(|(id, _)| *id)
|
.map(|(id, _)| *id)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
|
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
|
||||||
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);
|
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use crate::CliprdrError;
|
|||||||
// url encode and decode is needed
|
// url encode and decode is needed
|
||||||
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
|
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
|
||||||
|
|
||||||
pub(super) fn encode_path_to_uri(path: &PathBuf) -> String {
|
pub(super) fn encode_path_to_uri(path: &PathBuf) -> io::Result<String> {
|
||||||
let encoded = percent_encoding::percent_encode(path.to_str().unwrap().as_bytes(), &ENCODE_SET)
|
let encoded =
|
||||||
.to_string();
|
percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string();
|
||||||
format!("file://{}", encoded)
|
format!("file://{}", encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ mod uri_test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_conversion() {
|
fn test_conversion() {
|
||||||
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
|
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
|
||||||
let uri = super::encode_path_to_uri(&path);
|
let uri = super::encode_path_to_uri(&path).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
uri,
|
uri,
|
||||||
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||||
|
|||||||
@@ -89,7 +89,13 @@ impl SysClipboard for X11Clipboard {
|
|||||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||||
*self.former_file_list.lock() = paths.to_vec();
|
*self.former_file_list.lock() = paths.to_vec();
|
||||||
|
|
||||||
let uri_list: Vec<String> = paths.iter().map(encode_path_to_uri).collect();
|
let uri_list: Vec<String> = {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for path in paths {
|
||||||
|
v.push(encode_path_to_uri(path)?);
|
||||||
|
}
|
||||||
|
v
|
||||||
|
};
|
||||||
let uri_list = uri_list.join("\n");
|
let uri_list = uri_list.join("\n");
|
||||||
let text_uri_list_data = uri_list.as_bytes().to_vec();
|
let text_uri_list_data = uri_list.as_bytes().to_vec();
|
||||||
let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat();
|
let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat();
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
#![allow(deref_nullptr)]
|
#![allow(deref_nullptr)]
|
||||||
|
|
||||||
use std::{
|
|
||||||
boxed::Box,
|
|
||||||
ffi::{CStr, CString},
|
|
||||||
result::Result,
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
||||||
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
||||||
};
|
};
|
||||||
use hbb_common::log;
|
use hbb_common::log;
|
||||||
|
use std::{
|
||||||
|
boxed::Box,
|
||||||
|
ffi::{CStr, CString},
|
||||||
|
result::Result,
|
||||||
|
};
|
||||||
|
|
||||||
// only used error code will be recorded here
|
// only used error code will be recorded here
|
||||||
/// success
|
/// success
|
||||||
@@ -779,7 +779,7 @@ pub fn server_format_list(
|
|||||||
} else {
|
} else {
|
||||||
let n = match CString::new(format.1) {
|
let n = match CString::new(format.1) {
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(_) => CString::new("").unwrap(),
|
Err(_) => CString::new("").unwrap_or_default(),
|
||||||
};
|
};
|
||||||
CLIPRDR_FORMAT {
|
CLIPRDR_FORMAT {
|
||||||
formatId: format.0 as UINT32,
|
formatId: format.0 as UINT32,
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ appveyor = { repository = "pythoneer/enigo-85xiy" }
|
|||||||
serde = { version = "1.0", optional = true }
|
serde = { version = "1.0", optional = true }
|
||||||
serde_derive = { version = "1.0", optional = true }
|
serde_derive = { version = "1.0", optional = true }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rdev = { git = "https://github.com/fufesou/rdev" }
|
rdev = { git = "https://github.com/rustdesk-org/rdev" }
|
||||||
tfc = { git = "https://github.com/fufesou/The-Fat-Controller" }
|
tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller" }
|
||||||
hbb_common = { path = "../hbb_common" }
|
hbb_common = { path = "../hbb_common" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ impl MouseControllable for Enigo {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
match button {
|
match button {
|
||||||
MouseButton::Back => XBUTTON1 as u32 * WHEEL_DELTA as u32,
|
MouseButton::Back => XBUTTON1 as u32,
|
||||||
MouseButton::Forward => XBUTTON2 as u32 * WHEEL_DELTA as u32,
|
MouseButton::Forward => XBUTTON2 as u32,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ tokio-native-tls ="0.3"
|
|||||||
protobuf-codegen = { version = "3.4" }
|
protobuf-codegen = { version = "3.4" }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi"] }
|
winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi", "sysinfoapi"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
osascript = "0.3"
|
osascript = "0.3"
|
||||||
|
|||||||
@@ -271,6 +271,10 @@ enum ControlKey {
|
|||||||
RShift = 73;
|
RShift = 73;
|
||||||
RControl = 74;
|
RControl = 74;
|
||||||
RAlt = 75;
|
RAlt = 75;
|
||||||
|
VolumeMute = 76; // mainly used on mobile devices as controlled side
|
||||||
|
VolumeUp = 77;
|
||||||
|
VolumeDown = 78;
|
||||||
|
Power = 79; // mainly used on mobile devices as controlled side
|
||||||
CtrlAltDel = 100;
|
CtrlAltDel = 100;
|
||||||
LockScreen = 101;
|
LockScreen = 101;
|
||||||
}
|
}
|
||||||
@@ -314,6 +318,8 @@ message Hash {
|
|||||||
message Clipboard {
|
message Clipboard {
|
||||||
bool compress = 1;
|
bool compress = 1;
|
||||||
bytes content = 2;
|
bytes content = 2;
|
||||||
|
int32 width = 3;
|
||||||
|
int32 height = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FileType {
|
enum FileType {
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ message PunchHoleRequest {
|
|||||||
string licence_key = 3;
|
string licence_key = 3;
|
||||||
ConnType conn_type = 4;
|
ConnType conn_type = 4;
|
||||||
string token = 5;
|
string token = 5;
|
||||||
|
string version = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PunchHole {
|
message PunchHole {
|
||||||
bytes socket_addr = 1;
|
bytes socket_addr = 1;
|
||||||
string relay_server = 2;
|
string relay_server = 2;
|
||||||
NatType nat_type = 3;
|
NatType nat_type = 3;
|
||||||
string request_region = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message TestNatRequest {
|
message TestNatRequest {
|
||||||
@@ -52,7 +52,6 @@ message PunchHoleSent {
|
|||||||
string relay_server = 3;
|
string relay_server = 3;
|
||||||
NatType nat_type = 4;
|
NatType nat_type = 4;
|
||||||
string version = 5;
|
string version = 5;
|
||||||
string request_region = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message RegisterPk {
|
message RegisterPk {
|
||||||
@@ -92,6 +91,7 @@ message PunchHoleResponse {
|
|||||||
bool is_local = 6;
|
bool is_local = 6;
|
||||||
}
|
}
|
||||||
string other_failure = 7;
|
string other_failure = 7;
|
||||||
|
int32 feedback = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ConfigUpdate {
|
message ConfigUpdate {
|
||||||
@@ -108,7 +108,6 @@ message RequestRelay {
|
|||||||
string licence_key = 6;
|
string licence_key = 6;
|
||||||
ConnType conn_type = 7;
|
ConnType conn_type = 7;
|
||||||
string token = 8;
|
string token = 8;
|
||||||
string request_region = 9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message RelayResponse {
|
message RelayResponse {
|
||||||
@@ -121,7 +120,7 @@ message RelayResponse {
|
|||||||
}
|
}
|
||||||
string refuse_reason = 6;
|
string refuse_reason = 6;
|
||||||
string version = 7;
|
string version = 7;
|
||||||
string request_region = 8;
|
int32 feedback = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SoftwareUpdate { string url = 1; }
|
message SoftwareUpdate { string url = 1; }
|
||||||
@@ -133,7 +132,6 @@ message SoftwareUpdate { string url = 1; }
|
|||||||
message FetchLocalAddr {
|
message FetchLocalAddr {
|
||||||
bytes socket_addr = 1;
|
bytes socket_addr = 1;
|
||||||
string relay_server = 2;
|
string relay_server = 2;
|
||||||
string request_region = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message LocalAddr {
|
message LocalAddr {
|
||||||
@@ -142,7 +140,6 @@ message LocalAddr {
|
|||||||
string relay_server = 3;
|
string relay_server = 3;
|
||||||
string id = 4;
|
string id = 4;
|
||||||
string version = 5;
|
string version = 5;
|
||||||
string request_region = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message PeerDiscovery {
|
message PeerDiscovery {
|
||||||
@@ -168,6 +165,10 @@ message KeyExchange {
|
|||||||
repeated bytes keys = 1;
|
repeated bytes keys = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message HealthCheck {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message RendezvousMessage {
|
message RendezvousMessage {
|
||||||
oneof union {
|
oneof union {
|
||||||
RegisterPeer register_peer = 6;
|
RegisterPeer register_peer = 6;
|
||||||
@@ -190,5 +191,6 @@ message RendezvousMessage {
|
|||||||
OnlineRequest online_request = 23;
|
OnlineRequest online_request = 23;
|
||||||
OnlineResponse online_response = 24;
|
OnlineResponse online_response = 24;
|
||||||
KeyExchange key_exchange = 25;
|
KeyExchange key_exchange = 25;
|
||||||
|
HealthCheck hc = 26;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{cell::RefCell, io};
|
use std::{cell::RefCell, io};
|
||||||
use zstd::bulk::{Compressor, Decompressor};
|
use zstd::bulk::Compressor;
|
||||||
|
|
||||||
// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
|
// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
|
||||||
// which is currently 22. Levels >= 20
|
// which is currently 22. Levels >= 20
|
||||||
@@ -7,7 +7,6 @@ use zstd::bulk::{Compressor, Decompressor};
|
|||||||
// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
|
// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL));
|
static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL));
|
||||||
static DECOMPRESSOR: RefCell<io::Result<Decompressor<'static>>> = RefCell::new(Decompressor::new());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compress(data: &[u8]) -> Vec<u8> {
|
pub fn compress(data: &[u8]) -> Vec<u8> {
|
||||||
@@ -31,27 +30,5 @@ pub fn compress(data: &[u8]) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn decompress(data: &[u8]) -> Vec<u8> {
|
pub fn decompress(data: &[u8]) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
zstd::decode_all(data).unwrap_or_default()
|
||||||
DECOMPRESSOR.with(|d| {
|
|
||||||
if let Ok(mut d) = d.try_borrow_mut() {
|
|
||||||
match &mut *d {
|
|
||||||
Ok(d) => {
|
|
||||||
const MAX: usize = 1024 * 1024 * 64;
|
|
||||||
const MIN: usize = 1024 * 1024;
|
|
||||||
let mut n = 30 * data.len();
|
|
||||||
n = n.clamp(MIN, MAX);
|
|
||||||
match d.decompress(data, n) {
|
|
||||||
Ok(res) => out = res,
|
|
||||||
Err(err) => {
|
|
||||||
crate::log::debug!("Failed to decompress: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
crate::log::debug!("Failed to get decompressor: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ lazy_static::lazy_static! {
|
|||||||
pub static ref DEFAULT_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
pub static ref DEFAULT_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||||
pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||||
pub static ref HARD_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
pub static ref HARD_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||||
|
pub static ref BUILDIN_SETTINGS: RwLock<HashMap<String, String>> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
@@ -487,7 +488,19 @@ pub fn load_path<T: serde::Serialize + serde::de::DeserializeOwned + Default + s
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn store_path<T: serde::Serialize>(path: PathBuf, cfg: T) -> crate::ResultType<()> {
|
pub fn store_path<T: serde::Serialize>(path: PathBuf, cfg: T) -> crate::ResultType<()> {
|
||||||
Ok(confy::store_path(path, cfg)?)
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
Ok(confy::store_path_perms(
|
||||||
|
path,
|
||||||
|
cfg,
|
||||||
|
fs::Permissions::from_mode(0o600),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
Ok(confy::store_path(path, cfg)?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -2088,6 +2101,20 @@ pub mod keys {
|
|||||||
pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str =
|
pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str =
|
||||||
"enable-android-software-encoding-half-scale";
|
"enable-android-software-encoding-half-scale";
|
||||||
|
|
||||||
|
// buildin options
|
||||||
|
pub const OPTION_DISPLAY_NAME: &str = "display-name";
|
||||||
|
pub const OPTION_DISABLE_UDP: &str = "disable-udp";
|
||||||
|
pub const OPTION_PRESET_USERNAME: &str = "preset-user-name";
|
||||||
|
pub const OPTION_PRESET_STRATEGY_NAME: &str = "preset-strategy-name";
|
||||||
|
pub const OPTION_REMOVE_PRESET_PASSWORD_WARNING: &str = "remove-preset-password-warning";
|
||||||
|
pub const OPTION_HIDE_SECURITY_SETTINGS: &str = "hide-security-settings";
|
||||||
|
pub const OPTION_HIDE_NETWORK_SETTINGS: &str = "hide-network-settings";
|
||||||
|
pub const OPTION_HIDE_SERVER_SETTINGS: &str = "hide-server-settings";
|
||||||
|
pub const OPTION_HIDE_PROXY_SETTINGS: &str = "hide-proxy-settings";
|
||||||
|
pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card";
|
||||||
|
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
|
||||||
|
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
|
||||||
|
|
||||||
// flutter local options
|
// flutter local options
|
||||||
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
||||||
pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting";
|
pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting";
|
||||||
@@ -2096,6 +2123,7 @@ pub mod keys {
|
|||||||
pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible";
|
pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible";
|
||||||
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
|
pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type";
|
||||||
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
|
pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name";
|
||||||
|
pub const OPTION_ALLOW_REMOTE_CM_MODIFICATION: &str = "allow-remote-cm-modification";
|
||||||
|
|
||||||
// android floating window options
|
// android floating window options
|
||||||
pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window";
|
pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window";
|
||||||
@@ -2173,6 +2201,7 @@ pub mod keys {
|
|||||||
OPTION_KEEP_SCREEN_ON,
|
OPTION_KEEP_SCREEN_ON,
|
||||||
OPTION_DISABLE_GROUP_PANEL,
|
OPTION_DISABLE_GROUP_PANEL,
|
||||||
OPTION_PRE_ELEVATE_SERVICE,
|
OPTION_PRE_ELEVATE_SERVICE,
|
||||||
|
OPTION_ALLOW_REMOTE_CM_MODIFICATION,
|
||||||
];
|
];
|
||||||
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
||||||
pub const KEYS_SETTINGS: &[&str] = &[
|
pub const KEYS_SETTINGS: &[&str] = &[
|
||||||
@@ -2212,6 +2241,22 @@ pub mod keys {
|
|||||||
OPTION_ENABLE_DIRECTX_CAPTURE,
|
OPTION_ENABLE_DIRECTX_CAPTURE,
|
||||||
OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE,
|
OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// BUILDIN_SETTINGS
|
||||||
|
pub const KEYS_BUILDIN_SETTINGS: &[&str] = &[
|
||||||
|
OPTION_DISPLAY_NAME,
|
||||||
|
OPTION_DISABLE_UDP,
|
||||||
|
OPTION_PRESET_USERNAME,
|
||||||
|
OPTION_PRESET_STRATEGY_NAME,
|
||||||
|
OPTION_REMOVE_PRESET_PASSWORD_WARNING,
|
||||||
|
OPTION_HIDE_SECURITY_SETTINGS,
|
||||||
|
OPTION_HIDE_NETWORK_SETTINGS,
|
||||||
|
OPTION_HIDE_SERVER_SETTINGS,
|
||||||
|
OPTION_HIDE_PROXY_SETTINGS,
|
||||||
|
OPTION_HIDE_USERNAME_ON_CARD,
|
||||||
|
OPTION_HIDE_HELP_CARDS,
|
||||||
|
OPTION_DEFAULT_CONNECT_PASSWORD,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn common_load<
|
pub fn common_load<
|
||||||
@@ -2471,4 +2516,26 @@ mod tests {
|
|||||||
assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str");
|
assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_store_load() {
|
||||||
|
let peerconfig_id = "123456789";
|
||||||
|
let cfg: PeerConfig = Default::default();
|
||||||
|
cfg.store(&peerconfig_id);
|
||||||
|
assert_eq!(PeerConfig::load(&peerconfig_id), cfg);
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
assert_eq!(
|
||||||
|
// ignore file type information by masking with 0o777 (see https://stackoverflow.com/a/50045872)
|
||||||
|
fs::metadata(PeerConfig::path(&peerconfig_id))
|
||||||
|
.expect("reading metadata failed")
|
||||||
|
.permissions()
|
||||||
|
.mode()
|
||||||
|
& 0o777,
|
||||||
|
0o600
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,40 +85,19 @@ pub fn get_display_server_of_session(session: &str) -> String {
|
|||||||
run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
|
run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
|
||||||
// Check session type of the session
|
// Check session type of the session
|
||||||
{
|
{
|
||||||
let display_server = String::from_utf8_lossy(&output.stdout)
|
String::from_utf8_lossy(&output.stdout)
|
||||||
.replace("Type=", "")
|
.replace("Type=", "")
|
||||||
.trim_end()
|
.trim_end()
|
||||||
.into();
|
.into()
|
||||||
if display_server == "tty" {
|
|
||||||
// If the type is tty...
|
|
||||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session]))
|
|
||||||
// Get the tty number
|
|
||||||
{
|
|
||||||
let tty: String = String::from_utf8_lossy(&output.stdout)
|
|
||||||
.replace("TTY=", "")
|
|
||||||
.trim_end()
|
|
||||||
.into();
|
|
||||||
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() != "" {
|
|
||||||
// If it is, manually return "x11", otherwise return tty
|
|
||||||
return "x11".to_owned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
display_server
|
|
||||||
} else {
|
} else {
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
};
|
};
|
||||||
if display_server.is_empty() || display_server == "tty" {
|
if display_server.is_empty() || display_server == "tty" {
|
||||||
// loginctl has not given the expected output. try something else.
|
|
||||||
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
|
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
|
||||||
display_server = sestype;
|
if !sestype.is_empty() {
|
||||||
|
return sestype.to_lowercase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if display_server == "" {
|
|
||||||
display_server = "x11".to_owned();
|
display_server = "x11".to_owned();
|
||||||
}
|
}
|
||||||
display_server.to_lowercase()
|
display_server.to_lowercase()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
os::windows::raw::HANDLE,
|
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
@@ -17,7 +16,7 @@ use winapi::{
|
|||||||
sysinfoapi::VerSetConditionMask,
|
sysinfoapi::VerSetConditionMask,
|
||||||
winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0},
|
winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0},
|
||||||
winnt::{
|
winnt::{
|
||||||
OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION,
|
HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION,
|
||||||
VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR,
|
VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.2.6"
|
version = "1.2.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ dirs = "5.0"
|
|||||||
md5 = "0.7"
|
md5 = "0.7"
|
||||||
winapi = { version = "0.3", features = ["winbase"] }
|
winapi = { version = "0.3", features = ["winbase"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
|
||||||
|
|
||||||
[package.metadata.winres]
|
[package.metadata.winres]
|
||||||
LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
|
LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
|
||||||
ProductName = "RustDesk"
|
ProductName = "RustDesk"
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use std::{
|
|||||||
use bin_reader::BinaryReader;
|
use bin_reader::BinaryReader;
|
||||||
|
|
||||||
pub mod bin_reader;
|
pub mod bin_reader;
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod ui;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
|
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
|
||||||
@@ -17,6 +19,8 @@ const APP_METADATA_CONFIG: &str = "meta.toml";
|
|||||||
const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = ";
|
const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = ";
|
||||||
const APP_PREFIX: &str = "rustdesk";
|
const APP_PREFIX: &str = "rustdesk";
|
||||||
const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
|
const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
|
||||||
|
#[cfg(windows)]
|
||||||
|
const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW";
|
||||||
|
|
||||||
fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
|
fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
|
||||||
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
|
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
|
||||||
@@ -55,7 +59,13 @@ fn write_meta(dir: &PathBuf, ts: u64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<PathBuf> {
|
fn setup(
|
||||||
|
reader: BinaryReader,
|
||||||
|
dir: Option<PathBuf>,
|
||||||
|
clear: bool,
|
||||||
|
_args: &Vec<String>,
|
||||||
|
_ui: &mut bool,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
let dir = if let Some(dir) = dir {
|
let dir = if let Some(dir) = dir {
|
||||||
dir
|
dir
|
||||||
} else {
|
} else {
|
||||||
@@ -70,6 +80,11 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
|
|||||||
|
|
||||||
let mut ts = 0;
|
let mut ts = 0;
|
||||||
if clear || !is_timestamp_matches(&dir, &mut ts) {
|
if clear || !is_timestamp_matches(&dir, &mut ts) {
|
||||||
|
#[cfg(windows)]
|
||||||
|
if _args.is_empty() {
|
||||||
|
*_ui = true;
|
||||||
|
ui::setup();
|
||||||
|
}
|
||||||
std::fs::remove_dir_all(&dir).ok();
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
}
|
}
|
||||||
for file in reader.files.iter() {
|
for file in reader.files.iter() {
|
||||||
@@ -83,7 +98,7 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
|
|||||||
Some(dir.join(&reader.exe))
|
Some(dir.join(&reader.exe))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(path: PathBuf, args: Vec<String>) {
|
fn execute(path: PathBuf, args: Vec<String>, _ui: bool) {
|
||||||
println!("executing {}", path.display());
|
println!("executing {}", path.display());
|
||||||
// setup env
|
// setup env
|
||||||
let exe = std::env::current_exe().unwrap_or_default();
|
let exe = std::env::current_exe().unwrap_or_default();
|
||||||
@@ -95,13 +110,28 @@ fn execute(path: PathBuf, args: Vec<String>) {
|
|||||||
{
|
{
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
||||||
|
if _ui {
|
||||||
|
cmd.env(SET_FOREGROUND_WINDOW_ENV_KEY, "1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cmd.env(APPNAME_RUNTIME_ENV_KEY, exe_name)
|
let _child = cmd
|
||||||
|
.env(APPNAME_RUNTIME_ENV_KEY, exe_name)
|
||||||
.stdin(Stdio::inherit())
|
.stdin(Stdio::inherit())
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
.spawn()
|
.spawn();
|
||||||
.ok();
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
if _ui {
|
||||||
|
match _child {
|
||||||
|
Ok(child) => unsafe {
|
||||||
|
winapi::um::winuser::AllowSetForegroundWindow(child.id() as u32);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -119,18 +149,21 @@ fn main() {
|
|||||||
let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe");
|
let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe");
|
||||||
let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe");
|
let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe");
|
||||||
|
|
||||||
|
let mut ui = false;
|
||||||
let reader = BinaryReader::default();
|
let reader = BinaryReader::default();
|
||||||
if let Some(exe) = setup(
|
if let Some(exe) = setup(
|
||||||
reader,
|
reader,
|
||||||
None,
|
None,
|
||||||
click_setup || args.contains(&"--silent-install".to_owned()),
|
click_setup || args.contains(&"--silent-install".to_owned()),
|
||||||
|
&args,
|
||||||
|
&mut ui,
|
||||||
) {
|
) {
|
||||||
if click_setup {
|
if click_setup {
|
||||||
args = vec!["--install".to_owned()];
|
args = vec!["--install".to_owned()];
|
||||||
} else if quick_support {
|
} else if quick_support {
|
||||||
args = vec!["--quick_support".to_owned()];
|
args = vec!["--quick_support".to_owned()];
|
||||||
}
|
}
|
||||||
execute(exe, args);
|
execute(exe, args, ui);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
libs/portable/src/res/label.png
Normal file
BIN
libs/portable/src/res/label.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
libs/portable/src/res/spin.gif
Normal file
BIN
libs/portable/src/res/spin.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
232
libs/portable/src/ui.rs
Normal file
232
libs/portable/src/ui.rs
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
use native_windows_gui as nwg;
|
||||||
|
use nwg::NativeUi;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
const GIF_DATA: &[u8] = include_bytes!("./res/spin.gif");
|
||||||
|
const LABEL_DATA: &[u8] = include_bytes!("./res/label.png");
|
||||||
|
const GIF_SIZE: i32 = 32;
|
||||||
|
const BG_COLOR: [u8; 3] = [90, 90, 120];
|
||||||
|
const BORDER_COLOR: [u8; 3] = [40, 40, 40];
|
||||||
|
const GIF_DELAY: u64 = 30;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BasicApp {
|
||||||
|
window: nwg::Window,
|
||||||
|
|
||||||
|
border_image: nwg::ImageFrame,
|
||||||
|
bg_image: nwg::ImageFrame,
|
||||||
|
gif_image: nwg::ImageFrame,
|
||||||
|
label_image: nwg::ImageFrame,
|
||||||
|
|
||||||
|
border_layout: nwg::GridLayout,
|
||||||
|
bg_layout: nwg::GridLayout,
|
||||||
|
inner_layout: nwg::GridLayout,
|
||||||
|
|
||||||
|
timer: nwg::AnimationTimer,
|
||||||
|
decoder: nwg::ImageDecoder,
|
||||||
|
gif_index: RefCell<usize>,
|
||||||
|
gif_images: RefCell<Vec<nwg::Bitmap>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BasicApp {
|
||||||
|
fn exit(&self) {
|
||||||
|
self.timer.stop();
|
||||||
|
nwg::stop_thread_dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_gif(&self) -> Result<(), nwg::NwgError> {
|
||||||
|
let image_source = self.decoder.from_stream(GIF_DATA)?;
|
||||||
|
for frame_index in 0..image_source.frame_count() {
|
||||||
|
let image_data = image_source.frame(frame_index)?;
|
||||||
|
let image_data = self
|
||||||
|
.decoder
|
||||||
|
.resize_image(&image_data, [GIF_SIZE as u32, GIF_SIZE as u32])?;
|
||||||
|
let bmp = image_data.as_bitmap()?;
|
||||||
|
self.gif_images.borrow_mut().push(bmp);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_gif(&self) -> Result<(), nwg::NwgError> {
|
||||||
|
let images = self.gif_images.borrow();
|
||||||
|
if images.len() == 0 {
|
||||||
|
return Err(nwg::NwgError::ImageDecoderError(
|
||||||
|
-1,
|
||||||
|
"no gif images".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let image_index = *self.gif_index.borrow() % images.len();
|
||||||
|
self.gif_image.set_bitmap(Some(&images[image_index]));
|
||||||
|
*self.gif_index.borrow_mut() = (image_index + 1) % images.len();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_timer(&self) {
|
||||||
|
self.timer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod basic_app_ui {
|
||||||
|
use super::*;
|
||||||
|
use native_windows_gui::{self as nwg, Bitmap};
|
||||||
|
use nwg::{Event, GridLayoutItem};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
pub struct BasicAppUi {
|
||||||
|
inner: Rc<BasicApp>,
|
||||||
|
default_handler: RefCell<Vec<nwg::EventHandler>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl nwg::NativeUi<BasicAppUi> for BasicApp {
|
||||||
|
fn build_ui(mut data: BasicApp) -> Result<BasicAppUi, nwg::NwgError> {
|
||||||
|
data.decoder = nwg::ImageDecoder::new()?;
|
||||||
|
let col_cnt: i32 = 7;
|
||||||
|
let row_cnt: i32 = 3;
|
||||||
|
let border_width: i32 = 1;
|
||||||
|
let window_size = (
|
||||||
|
GIF_SIZE * col_cnt + 2 * border_width,
|
||||||
|
GIF_SIZE * row_cnt + 2 * border_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
nwg::Window::builder()
|
||||||
|
.flags(nwg::WindowFlags::POPUP | nwg::WindowFlags::VISIBLE)
|
||||||
|
.size(window_size)
|
||||||
|
.center(true)
|
||||||
|
.build(&mut data.window)?;
|
||||||
|
|
||||||
|
nwg::ImageFrame::builder()
|
||||||
|
.parent(&data.window)
|
||||||
|
.size(window_size)
|
||||||
|
.background_color(Some(BORDER_COLOR))
|
||||||
|
.build(&mut data.border_image)?;
|
||||||
|
|
||||||
|
nwg::ImageFrame::builder()
|
||||||
|
.parent(&data.border_image)
|
||||||
|
.size((row_cnt * GIF_SIZE, col_cnt * GIF_SIZE))
|
||||||
|
.background_color(Some(BG_COLOR))
|
||||||
|
.build(&mut data.bg_image)?;
|
||||||
|
|
||||||
|
nwg::ImageFrame::builder()
|
||||||
|
.parent(&data.bg_image)
|
||||||
|
.size((GIF_SIZE, GIF_SIZE))
|
||||||
|
.background_color(Some(BG_COLOR))
|
||||||
|
.build(&mut data.gif_image)?;
|
||||||
|
|
||||||
|
nwg::ImageFrame::builder()
|
||||||
|
.parent(&data.bg_image)
|
||||||
|
.background_color(Some(BG_COLOR))
|
||||||
|
.bitmap(Some(&Bitmap::from_bin(LABEL_DATA)?))
|
||||||
|
.build(&mut data.label_image)?;
|
||||||
|
|
||||||
|
nwg::AnimationTimer::builder()
|
||||||
|
.parent(&data.window)
|
||||||
|
.interval(std::time::Duration::from_millis(GIF_DELAY))
|
||||||
|
.build(&mut data.timer)?;
|
||||||
|
|
||||||
|
// Wrap-up
|
||||||
|
let ui = BasicAppUi {
|
||||||
|
inner: Rc::new(data),
|
||||||
|
default_handler: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Layouts
|
||||||
|
nwg::GridLayout::builder()
|
||||||
|
.parent(&ui.window)
|
||||||
|
.spacing(0)
|
||||||
|
.margin([0, 0, 0, 0])
|
||||||
|
.max_column(Some(1))
|
||||||
|
.max_row(Some(1))
|
||||||
|
.child_item(GridLayoutItem::new(&ui.border_image, 0, 0, 1, 1))
|
||||||
|
.build(&ui.border_layout)?;
|
||||||
|
|
||||||
|
nwg::GridLayout::builder()
|
||||||
|
.parent(&ui.border_image)
|
||||||
|
.spacing(0)
|
||||||
|
.margin([
|
||||||
|
border_width as _,
|
||||||
|
border_width as _,
|
||||||
|
border_width as _,
|
||||||
|
border_width as _,
|
||||||
|
])
|
||||||
|
.max_column(Some(1))
|
||||||
|
.max_row(Some(1))
|
||||||
|
.child_item(GridLayoutItem::new(&ui.bg_image, 0, 0, 1, 1))
|
||||||
|
.build(&ui.bg_layout)?;
|
||||||
|
|
||||||
|
nwg::GridLayout::builder()
|
||||||
|
.parent(&ui.bg_image)
|
||||||
|
.spacing(0)
|
||||||
|
.margin([0, 0, 0, 0])
|
||||||
|
.max_column(Some(col_cnt as _))
|
||||||
|
.max_row(Some(row_cnt as _))
|
||||||
|
.child_item(GridLayoutItem::new(&ui.gif_image, 2, 1, 1, 1))
|
||||||
|
.child_item(GridLayoutItem::new(&ui.label_image, 3, 1, 3, 1))
|
||||||
|
.build(&ui.inner_layout)?;
|
||||||
|
|
||||||
|
// Events
|
||||||
|
let evt_ui = Rc::downgrade(&ui.inner);
|
||||||
|
let handle_events = move |evt, _evt_data, _handle| {
|
||||||
|
if let Some(evt_ui) = evt_ui.upgrade().as_mut() {
|
||||||
|
match evt {
|
||||||
|
Event::OnWindowClose => {
|
||||||
|
evt_ui.exit();
|
||||||
|
}
|
||||||
|
Event::OnTimerTick => {
|
||||||
|
if let Err(e) = evt_ui.update_gif() {
|
||||||
|
eprintln!("{:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.default_handler
|
||||||
|
.borrow_mut()
|
||||||
|
.push(nwg::full_bind_event_handler(
|
||||||
|
&ui.window.handle,
|
||||||
|
handle_events,
|
||||||
|
));
|
||||||
|
|
||||||
|
return Ok(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BasicAppUi {
|
||||||
|
/// To make sure that everything is freed without issues, the default handler must be unbound.
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut handlers = self.default_handler.borrow_mut();
|
||||||
|
for handler in handlers.drain(0..) {
|
||||||
|
nwg::unbind_event_handler(&handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for BasicAppUi {
|
||||||
|
type Target = BasicApp;
|
||||||
|
|
||||||
|
fn deref(&self) -> &BasicApp {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui() -> Result<(), nwg::NwgError> {
|
||||||
|
nwg::init()?;
|
||||||
|
let app = BasicApp::build_ui(Default::default())?;
|
||||||
|
app.load_gif()?;
|
||||||
|
app.start_timer();
|
||||||
|
nwg::dispatch_thread_events();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup() {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Err(e) = ui() {
|
||||||
|
eprintln!("{:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
anyhow::anyhow,
|
anyhow::anyhow,
|
||||||
bail,
|
bail,
|
||||||
config::{keys::OPTION_ENABLE_HWCODEC, option2bool, Config, PeerConfig},
|
config::{option2bool, Config, PeerConfig},
|
||||||
lazy_static, log,
|
lazy_static, log,
|
||||||
message_proto::{
|
message_proto::{
|
||||||
supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames,
|
supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames,
|
||||||
@@ -836,7 +836,9 @@ impl Decoder {
|
|||||||
|
|
||||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
||||||
pub fn enable_hwcodec_option() -> bool {
|
pub fn enable_hwcodec_option() -> bool {
|
||||||
if cfg!(windows) || cfg!(target_os = "linux") || cfg!(target_os = "android") {
|
use hbb_common::config::keys::OPTION_ENABLE_HWCODEC;
|
||||||
|
|
||||||
|
if !cfg!(target_os = "ios") {
|
||||||
return option2bool(
|
return option2bool(
|
||||||
OPTION_ENABLE_HWCODEC,
|
OPTION_ENABLE_HWCODEC,
|
||||||
&Config::get_option(OPTION_ENABLE_HWCODEC),
|
&Config::get_option(OPTION_ENABLE_HWCODEC),
|
||||||
@@ -846,6 +848,8 @@ pub fn enable_hwcodec_option() -> bool {
|
|||||||
}
|
}
|
||||||
#[cfg(feature = "vram")]
|
#[cfg(feature = "vram")]
|
||||||
pub fn enable_vram_option(encode: bool) -> bool {
|
pub fn enable_vram_option(encode: bool) -> bool {
|
||||||
|
use hbb_common::config::keys::OPTION_ENABLE_HWCODEC;
|
||||||
|
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
let enable = option2bool(
|
let enable = option2bool(
|
||||||
OPTION_ENABLE_HWCODEC,
|
OPTION_ENABLE_HWCODEC,
|
||||||
|
|||||||
@@ -32,13 +32,16 @@ pub fn convert_to_yuv(
|
|||||||
dst_fmt.h
|
dst_fmt.h
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if src_pixfmt == crate::Pixfmt::BGRA || src_pixfmt == crate::Pixfmt::RGBA {
|
if src_pixfmt == crate::Pixfmt::BGRA
|
||||||
|
|| src_pixfmt == crate::Pixfmt::RGBA
|
||||||
|
|| src_pixfmt == crate::Pixfmt::RGB565LE
|
||||||
|
{
|
||||||
// stride is calculated, not real, so we need to check it
|
// stride is calculated, not real, so we need to check it
|
||||||
if src_stride[0] < src_width * 4 {
|
if src_stride[0] < src_width * src_pixfmt.bytes_per_pixel() {
|
||||||
bail!(
|
bail!(
|
||||||
"src_stride[0] < src_width * 4: {} < {}",
|
"src_stride too small: {} < {}",
|
||||||
src_stride[0],
|
src_stride[0],
|
||||||
src_width * 4
|
src_width * src_pixfmt.bytes_per_pixel()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if src.len() < src_stride[0] * src_height {
|
if src.len() < src_stride[0] * src_height {
|
||||||
@@ -51,19 +54,26 @@ pub fn convert_to_yuv(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let align = |x: usize| (x + 63) / 64 * 64;
|
let align = |x: usize| (x + 63) / 64 * 64;
|
||||||
|
let unsupported = format!(
|
||||||
|
"unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}",
|
||||||
|
dst_fmt.pixfmt
|
||||||
|
);
|
||||||
|
|
||||||
match (src_pixfmt, dst_fmt.pixfmt) {
|
match (src_pixfmt, dst_fmt.pixfmt) {
|
||||||
(crate::Pixfmt::BGRA, crate::Pixfmt::I420) | (crate::Pixfmt::RGBA, crate::Pixfmt::I420) => {
|
(crate::Pixfmt::BGRA, crate::Pixfmt::I420)
|
||||||
|
| (crate::Pixfmt::RGBA, crate::Pixfmt::I420)
|
||||||
|
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::I420) => {
|
||||||
let dst_stride_y = dst_fmt.stride[0];
|
let dst_stride_y = dst_fmt.stride[0];
|
||||||
let dst_stride_uv = dst_fmt.stride[1];
|
let dst_stride_uv = dst_fmt.stride[1];
|
||||||
dst.resize(dst_fmt.h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
|
dst.resize(dst_fmt.h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety
|
||||||
let dst_y = dst.as_mut_ptr();
|
let dst_y = dst.as_mut_ptr();
|
||||||
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
||||||
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
||||||
let f = if src_pixfmt == crate::Pixfmt::BGRA {
|
let f = match src_pixfmt {
|
||||||
ARGBToI420
|
crate::Pixfmt::BGRA => ARGBToI420,
|
||||||
} else {
|
crate::Pixfmt::RGBA => ABGRToI420,
|
||||||
ABGRToI420
|
crate::Pixfmt::RGB565LE => RGB565ToI420,
|
||||||
|
_ => bail!(unsupported),
|
||||||
};
|
};
|
||||||
call_yuv!(f(
|
call_yuv!(f(
|
||||||
src.as_ptr(),
|
src.as_ptr(),
|
||||||
@@ -78,7 +88,9 @@ pub fn convert_to_yuv(
|
|||||||
src_height as _,
|
src_height as _,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
(crate::Pixfmt::BGRA, crate::Pixfmt::NV12) | (crate::Pixfmt::RGBA, crate::Pixfmt::NV12) => {
|
(crate::Pixfmt::BGRA, crate::Pixfmt::NV12)
|
||||||
|
| (crate::Pixfmt::RGBA, crate::Pixfmt::NV12)
|
||||||
|
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::NV12) => {
|
||||||
let dst_stride_y = dst_fmt.stride[0];
|
let dst_stride_y = dst_fmt.stride[0];
|
||||||
let dst_stride_uv = dst_fmt.stride[1];
|
let dst_stride_uv = dst_fmt.stride[1];
|
||||||
dst.resize(
|
dst.resize(
|
||||||
@@ -87,14 +99,33 @@ pub fn convert_to_yuv(
|
|||||||
);
|
);
|
||||||
let dst_y = dst.as_mut_ptr();
|
let dst_y = dst.as_mut_ptr();
|
||||||
let dst_uv = dst[dst_fmt.u..].as_mut_ptr();
|
let dst_uv = dst[dst_fmt.u..].as_mut_ptr();
|
||||||
let f = if src_pixfmt == crate::Pixfmt::BGRA {
|
let (input, input_stride) = match src_pixfmt {
|
||||||
ARGBToNV12
|
crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]),
|
||||||
} else {
|
crate::Pixfmt::RGBA => (src.as_ptr(), src_stride[0]),
|
||||||
ABGRToNV12
|
crate::Pixfmt::RGB565LE => {
|
||||||
|
let mid_stride = src_width * 4;
|
||||||
|
mid_data.resize(mid_stride * src_height, 0);
|
||||||
|
call_yuv!(RGB565ToARGB(
|
||||||
|
src.as_ptr(),
|
||||||
|
src_stride[0] as _,
|
||||||
|
mid_data.as_mut_ptr(),
|
||||||
|
mid_stride as _,
|
||||||
|
src_width as _,
|
||||||
|
src_height as _,
|
||||||
|
));
|
||||||
|
(mid_data.as_ptr(), mid_stride)
|
||||||
|
}
|
||||||
|
_ => bail!(unsupported),
|
||||||
|
};
|
||||||
|
let f = match src_pixfmt {
|
||||||
|
crate::Pixfmt::BGRA => ARGBToNV12,
|
||||||
|
crate::Pixfmt::RGBA => ABGRToNV12,
|
||||||
|
crate::Pixfmt::RGB565LE => ARGBToNV12,
|
||||||
|
_ => bail!(unsupported),
|
||||||
};
|
};
|
||||||
call_yuv!(f(
|
call_yuv!(f(
|
||||||
src.as_ptr(),
|
input,
|
||||||
src_stride[0] as _,
|
input_stride as _,
|
||||||
dst_y,
|
dst_y,
|
||||||
dst_stride_y as _,
|
dst_stride_y as _,
|
||||||
dst_uv,
|
dst_uv,
|
||||||
@@ -103,7 +134,9 @@ pub fn convert_to_yuv(
|
|||||||
src_height as _,
|
src_height as _,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
(crate::Pixfmt::BGRA, crate::Pixfmt::I444) | (crate::Pixfmt::RGBA, crate::Pixfmt::I444) => {
|
(crate::Pixfmt::BGRA, crate::Pixfmt::I444)
|
||||||
|
| (crate::Pixfmt::RGBA, crate::Pixfmt::I444)
|
||||||
|
| (crate::Pixfmt::RGB565LE, crate::Pixfmt::I444) => {
|
||||||
let dst_stride_y = dst_fmt.stride[0];
|
let dst_stride_y = dst_fmt.stride[0];
|
||||||
let dst_stride_u = dst_fmt.stride[1];
|
let dst_stride_u = dst_fmt.stride[1];
|
||||||
let dst_stride_v = dst_fmt.stride[2];
|
let dst_stride_v = dst_fmt.stride[2];
|
||||||
@@ -115,23 +148,39 @@ pub fn convert_to_yuv(
|
|||||||
let dst_y = dst.as_mut_ptr();
|
let dst_y = dst.as_mut_ptr();
|
||||||
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
let dst_u = dst[dst_fmt.u..].as_mut_ptr();
|
||||||
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
let dst_v = dst[dst_fmt.v..].as_mut_ptr();
|
||||||
let src = if src_pixfmt == crate::Pixfmt::BGRA {
|
let (input, input_stride) = match src_pixfmt {
|
||||||
src
|
crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]),
|
||||||
} else {
|
crate::Pixfmt::RGBA => {
|
||||||
mid_data.resize(src.len(), 0);
|
mid_data.resize(src.len(), 0);
|
||||||
call_yuv!(ABGRToARGB(
|
call_yuv!(ABGRToARGB(
|
||||||
src.as_ptr(),
|
src.as_ptr(),
|
||||||
src_stride[0] as _,
|
src_stride[0] as _,
|
||||||
mid_data.as_mut_ptr(),
|
mid_data.as_mut_ptr(),
|
||||||
src_stride[0] as _,
|
src_stride[0] as _,
|
||||||
src_width as _,
|
src_width as _,
|
||||||
src_height as _,
|
src_height as _,
|
||||||
));
|
));
|
||||||
mid_data
|
(mid_data.as_ptr(), src_stride[0])
|
||||||
|
}
|
||||||
|
crate::Pixfmt::RGB565LE => {
|
||||||
|
let mid_stride = src_width * 4;
|
||||||
|
mid_data.resize(mid_stride * src_height, 0);
|
||||||
|
call_yuv!(RGB565ToARGB(
|
||||||
|
src.as_ptr(),
|
||||||
|
src_stride[0] as _,
|
||||||
|
mid_data.as_mut_ptr(),
|
||||||
|
mid_stride as _,
|
||||||
|
src_width as _,
|
||||||
|
src_height as _,
|
||||||
|
));
|
||||||
|
(mid_data.as_ptr(), mid_stride)
|
||||||
|
}
|
||||||
|
_ => bail!(unsupported),
|
||||||
};
|
};
|
||||||
|
|
||||||
call_yuv!(ARGBToI444(
|
call_yuv!(ARGBToI444(
|
||||||
src.as_ptr(),
|
input,
|
||||||
src_stride[0] as _,
|
input_stride as _,
|
||||||
dst_y,
|
dst_y,
|
||||||
dst_stride_y as _,
|
dst_stride_y as _,
|
||||||
dst_u,
|
dst_u,
|
||||||
@@ -143,10 +192,7 @@ pub fn convert_to_yuv(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
bail!(
|
bail!(unsupported);
|
||||||
"convert not support, {src_pixfmt:?} -> {:?}",
|
|
||||||
dst_fmt.pixfmt
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -192,19 +192,21 @@ impl EncoderApi for HwRamEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn support_abr(&self) -> bool {
|
fn support_abr(&self) -> bool {
|
||||||
["qsv", "vaapi", "mediacodec"]
|
["qsv", "vaapi", "mediacodec", "videotoolbox"]
|
||||||
.iter()
|
.iter()
|
||||||
.all(|&x| !self.config.name.contains(x))
|
.all(|&x| !self.config.name.contains(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn support_changing_quality(&self) -> bool {
|
fn support_changing_quality(&self) -> bool {
|
||||||
["vaapi", "mediacodec"]
|
["vaapi", "mediacodec", "videotoolbox"]
|
||||||
.iter()
|
.iter()
|
||||||
.all(|&x| !self.config.name.contains(x))
|
.all(|&x| !self.config.name.contains(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn latency_free(&self) -> bool {
|
fn latency_free(&self) -> bool {
|
||||||
!self.config.name.contains("mediacodec")
|
["mediacodec", "videotoolbox"]
|
||||||
|
.iter()
|
||||||
|
.all(|&x| !self.config.name.contains(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_hardware(&self) -> bool {
|
fn is_hardware(&self) -> bool {
|
||||||
@@ -501,12 +503,12 @@ pub struct HwCodecConfig {
|
|||||||
// portable: ui start check process, check process send to ui
|
// portable: ui start check process, check process send to ui
|
||||||
// sciter and unilink: get from ipc server
|
// sciter and unilink: get from ipc server
|
||||||
impl HwCodecConfig {
|
impl HwCodecConfig {
|
||||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn set(config: String) {
|
pub fn set(config: String) {
|
||||||
let config = serde_json::from_str(&config).unwrap_or_default();
|
let config = serde_json::from_str(&config).unwrap_or_default();
|
||||||
log::info!("set hwcodec config");
|
log::info!("set hwcodec config");
|
||||||
log::debug!("{config:?}");
|
log::debug!("{config:?}");
|
||||||
#[cfg(windows)]
|
#[cfg(any(windows, target_os = "macos"))]
|
||||||
hbb_common::config::common_store(&config, "_hwcodec");
|
hbb_common::config::common_store(&config, "_hwcodec");
|
||||||
*CONFIG.lock().unwrap() = Some(config);
|
*CONFIG.lock().unwrap() = Some(config);
|
||||||
*CONFIG_SET_BY_IPC.lock().unwrap() = true;
|
*CONFIG_SET_BY_IPC.lock().unwrap() = true;
|
||||||
@@ -578,7 +580,7 @@ impl HwCodecConfig {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(any(windows, target_os = "macos"))]
|
||||||
{
|
{
|
||||||
let config = CONFIG.lock().unwrap().clone();
|
let config = CONFIG.lock().unwrap().clone();
|
||||||
match config {
|
match config {
|
||||||
@@ -606,13 +608,13 @@ impl HwCodecConfig {
|
|||||||
{
|
{
|
||||||
CONFIG.lock().unwrap().clone().unwrap_or_default()
|
CONFIG.lock().unwrap().clone().unwrap_or_default()
|
||||||
}
|
}
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(target_os = "ios")]
|
||||||
{
|
{
|
||||||
HwCodecConfig::default()
|
HwCodecConfig::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn get_set_value() -> Option<HwCodecConfig> {
|
pub fn get_set_value() -> Option<HwCodecConfig> {
|
||||||
let set = CONFIG_SET_BY_IPC.lock().unwrap().clone();
|
let set = CONFIG_SET_BY_IPC.lock().unwrap().clone();
|
||||||
if set {
|
if set {
|
||||||
@@ -622,7 +624,7 @@ impl HwCodecConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn already_set() -> bool {
|
pub fn already_set() -> bool {
|
||||||
CONFIG_SET_BY_IPC.lock().unwrap().clone()
|
CONFIG_SET_BY_IPC.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
@@ -690,7 +692,7 @@ pub fn check_available_hwcodec() -> String {
|
|||||||
serde_json::to_string(&c).unwrap_or_default()
|
serde_json::to_string(&c).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn start_check_process() {
|
pub fn start_check_process() {
|
||||||
if !enable_hwcodec_option() || HwCodecConfig::already_set() {
|
if !enable_hwcodec_option() || HwCodecConfig::already_set() {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ impl Display {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently, wayland need to call wayland::clear() before call Display::all()
|
||||||
pub fn all() -> io::Result<Vec<Display>> {
|
pub fn all() -> io::Result<Vec<Display>> {
|
||||||
Ok(if super::is_x11() {
|
Ok(if super::is_x11() {
|
||||||
x11::Display::all()?
|
x11::Display::all()?
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ pub enum ImageFormat {
|
|||||||
ABGR,
|
ABGR,
|
||||||
ARGB,
|
ARGB,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct ImageRgb {
|
pub struct ImageRgb {
|
||||||
pub raw: Vec<u8>,
|
pub raw: Vec<u8>,
|
||||||
@@ -208,11 +209,27 @@ impl<'a> EncodeInput<'a> {
|
|||||||
pub enum Pixfmt {
|
pub enum Pixfmt {
|
||||||
BGRA,
|
BGRA,
|
||||||
RGBA,
|
RGBA,
|
||||||
|
RGB565LE,
|
||||||
I420,
|
I420,
|
||||||
NV12,
|
NV12,
|
||||||
I444,
|
I444,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Pixfmt {
|
||||||
|
pub fn bpp(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Pixfmt::BGRA | Pixfmt::RGBA => 32,
|
||||||
|
Pixfmt::RGB565LE => 16,
|
||||||
|
Pixfmt::I420 | Pixfmt::NV12 => 12,
|
||||||
|
Pixfmt::I444 => 24,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bytes_per_pixel(&self) -> usize {
|
||||||
|
(self.bpp() + 7) / 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EncodeYuvFormat {
|
pub struct EncodeYuvFormat {
|
||||||
pub pixfmt: Pixfmt,
|
pub pixfmt: Pixfmt,
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ impl TraitCapturer for Capturer {
|
|||||||
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
|
fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result<Frame<'a>> {
|
||||||
let width = self.width();
|
let width = self.width();
|
||||||
let height = self.height();
|
let height = self.height();
|
||||||
|
let pixfmt = self.0.display().pixfmt();
|
||||||
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
Ok(Frame::PixelBuffer(PixelBuffer::new(
|
||||||
self.0.frame()?,
|
self.0.frame()?,
|
||||||
Pixfmt::BGRA,
|
pixfmt,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
)))
|
)))
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ impl Capturer {
|
|||||||
pub fn new(display: Display) -> io::Result<Capturer> {
|
pub fn new(display: Display) -> io::Result<Capturer> {
|
||||||
// Calculate dimensions.
|
// Calculate dimensions.
|
||||||
|
|
||||||
let pixel_width = 4;
|
let pixel_width = display.pixfmt().bytes_per_pixel();
|
||||||
let rect = display.rect();
|
let rect = display.rect();
|
||||||
let size = (rect.w as usize) * (rect.h as usize) * pixel_width;
|
let size = (rect.w as usize) * (rect.h as usize) * pixel_width;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use super::ffi::*;
|
use super::ffi::*;
|
||||||
use super::Server;
|
use super::Server;
|
||||||
|
use crate::Pixfmt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Display {
|
pub struct Display {
|
||||||
@@ -10,6 +11,7 @@ pub struct Display {
|
|||||||
rect: Rect,
|
rect: Rect,
|
||||||
root: xcb_window_t,
|
root: xcb_window_t,
|
||||||
name: String,
|
name: String,
|
||||||
|
pixfmt: Pixfmt,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
@@ -27,6 +29,7 @@ impl Display {
|
|||||||
rect: Rect,
|
rect: Rect,
|
||||||
root: xcb_window_t,
|
root: xcb_window_t,
|
||||||
name: String,
|
name: String,
|
||||||
|
pixfmt: Pixfmt,
|
||||||
) -> Display {
|
) -> Display {
|
||||||
Display {
|
Display {
|
||||||
server,
|
server,
|
||||||
@@ -34,6 +37,7 @@ impl Display {
|
|||||||
rect,
|
rect,
|
||||||
root,
|
root,
|
||||||
name,
|
name,
|
||||||
|
pixfmt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,4 +63,8 @@ impl Display {
|
|||||||
pub fn name(&self) -> String {
|
pub fn name(&self) -> String {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn pixfmt(&self) -> Pixfmt {
|
||||||
|
self.pixfmt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,12 +82,24 @@ extern "C" {
|
|||||||
pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32;
|
pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32;
|
||||||
|
|
||||||
pub fn xcb_shm_query_version(c: *mut xcb_connection_t) -> xcb_shm_query_version_cookie_t;
|
pub fn xcb_shm_query_version(c: *mut xcb_connection_t) -> xcb_shm_query_version_cookie_t;
|
||||||
|
|
||||||
pub fn xcb_shm_query_version_reply(
|
pub fn xcb_shm_query_version_reply(
|
||||||
c: *mut xcb_connection_t,
|
c: *mut xcb_connection_t,
|
||||||
cookie: xcb_shm_query_version_cookie_t,
|
cookie: xcb_shm_query_version_cookie_t,
|
||||||
e: *mut *mut xcb_generic_error_t,
|
e: *mut *mut xcb_generic_error_t,
|
||||||
) -> *const xcb_shm_query_version_reply_t;
|
) -> *const xcb_shm_query_version_reply_t;
|
||||||
|
|
||||||
|
pub fn xcb_get_geometry_unchecked(
|
||||||
|
c: *mut xcb_connection_t,
|
||||||
|
drawable: xcb_drawable_t,
|
||||||
|
) -> xcb_get_geometry_cookie_t;
|
||||||
|
|
||||||
|
pub fn xcb_get_geometry_reply(
|
||||||
|
c: *mut xcb_connection_t,
|
||||||
|
cookie: xcb_get_geometry_cookie_t,
|
||||||
|
e: *mut *mut xcb_generic_error_t,
|
||||||
|
) -> *mut xcb_get_geometry_reply_t;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2;
|
pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2;
|
||||||
@@ -195,6 +207,12 @@ pub struct xcb_void_cookie_t {
|
|||||||
pub sequence: u32,
|
pub sequence: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct xcb_get_geometry_cookie_t {
|
||||||
|
pub sequence: u32,
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct xcb_generic_error_t {
|
pub struct xcb_generic_error_t {
|
||||||
pub response_type: u8,
|
pub response_type: u8,
|
||||||
@@ -248,3 +266,18 @@ pub struct xcb_shm_query_version_reply_t {
|
|||||||
pub pixmap_format: u8,
|
pub pixmap_format: u8,
|
||||||
pub pad0: [u8; 15],
|
pub pad0: [u8; 15],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct xcb_get_geometry_reply_t {
|
||||||
|
pub response_type: u8,
|
||||||
|
pub depth: u8,
|
||||||
|
pub sequence: u16,
|
||||||
|
pub length: u32,
|
||||||
|
pub root: xcb_window_t,
|
||||||
|
pub x: i16,
|
||||||
|
pub y: i16,
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
pub border_width: u16,
|
||||||
|
pub pad0: [u8; 2],
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::ffi::CString;
|
|||||||
use std::ptr;
|
use std::ptr;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::Pixfmt;
|
||||||
use hbb_common::libc;
|
use hbb_common::libc;
|
||||||
|
|
||||||
use super::ffi::*;
|
use super::ffi::*;
|
||||||
@@ -66,7 +67,7 @@ impl Iterator for DisplayIter {
|
|||||||
unsafe {
|
unsafe {
|
||||||
let data = &*inner.data;
|
let data = &*inner.data;
|
||||||
let name = get_atom_name(self.server.raw(), data.name);
|
let name = get_atom_name(self.server.raw(), data.name);
|
||||||
|
let pixfmt = get_pixfmt(self.server.raw(), root).unwrap_or(Pixfmt::BGRA);
|
||||||
let display = Display::new(
|
let display = Display::new(
|
||||||
self.server.clone(),
|
self.server.clone(),
|
||||||
data.primary != 0,
|
data.primary != 0,
|
||||||
@@ -78,6 +79,7 @@ impl Iterator for DisplayIter {
|
|||||||
},
|
},
|
||||||
root,
|
root,
|
||||||
name,
|
name,
|
||||||
|
pixfmt,
|
||||||
);
|
);
|
||||||
|
|
||||||
xcb_randr_monitor_info_next(inner);
|
xcb_randr_monitor_info_next(inner);
|
||||||
@@ -102,11 +104,7 @@ fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String {
|
|||||||
}
|
}
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut e: *mut xcb_generic_error_t = std::ptr::null_mut();
|
let mut e: *mut xcb_generic_error_t = std::ptr::null_mut();
|
||||||
let reply = xcb_get_atom_name_reply(
|
let reply = xcb_get_atom_name_reply(conn, xcb_get_atom_name(conn, atom), &mut e as _);
|
||||||
conn,
|
|
||||||
xcb_get_atom_name(conn, atom),
|
|
||||||
&mut e as _,
|
|
||||||
);
|
|
||||||
if reply == std::ptr::null() {
|
if reply == std::ptr::null() {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
@@ -121,3 +119,20 @@ fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String {
|
|||||||
empty
|
empty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe fn get_pixfmt(conn: *mut xcb_connection_t, root: xcb_window_t) -> Option<Pixfmt> {
|
||||||
|
let geo_cookie = xcb_get_geometry_unchecked(conn, root);
|
||||||
|
let geo = xcb_get_geometry_reply(conn, geo_cookie, ptr::null_mut());
|
||||||
|
if geo.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let depth = (*geo).depth;
|
||||||
|
libc::free(geo as _);
|
||||||
|
// now only support little endian
|
||||||
|
// https://github.com/FFmpeg/FFmpeg/blob/a9c05eb657d0d05f3ac79fe9973581a41b265a5e/libavdevice/xcbgrab.c#L519
|
||||||
|
match depth {
|
||||||
|
16 => Some(Pixfmt::RGB565LE),
|
||||||
|
32 => Some(Pixfmt::BGRA),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Virtual display may be used on computers that do not have a monitor.
|
|||||||
|
|
||||||
Win10 provides [Indirect Display Driver Model](https://msdn.microsoft.com/en-us/library/windows/hardware/mt761968(v=vs.85).aspx).
|
Win10 provides [Indirect Display Driver Model](https://msdn.microsoft.com/en-us/library/windows/hardware/mt761968(v=vs.85).aspx).
|
||||||
|
|
||||||
This lib uses [this project](https://github.com/fufesou/RustDeskIddDriver) as the driver.
|
This lib uses [this project](https://github.com/rustdesk-org/RustDeskIddDriver) as the driver.
|
||||||
|
|
||||||
|
|
||||||
**NOTE**: Versions before Win10 1607. Try follow [this method](https://github.com/fanxiushu/xdisp_virt/tree/master/indirect_display).
|
**NOTE**: Versions before Win10 1607. Try follow [this method](https://github.com/fanxiushu/xdisp_virt/tree/master/indirect_display).
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ if [ "$1" = configure ]; then
|
|||||||
parsedVersion=$(echo "${version//./}")
|
parsedVersion=$(echo "${version//./}")
|
||||||
mkdir -p /usr/lib/systemd/system/
|
mkdir -p /usr/lib/systemd/system/
|
||||||
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
|
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
|
||||||
|
# try fix error in Ubuntu 18.04
|
||||||
|
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.
|
||||||
|
# /usr/lib/systemd/system/rustdesk.service:10: Executable path is not absolute: pkill -f "rustdesk --"
|
||||||
|
if [ -e /usr/bin/pkill ]; then
|
||||||
|
sed -i "s|pkill|/usr/bin/pkill|g" /usr/lib/systemd/system/rustdesk.service
|
||||||
|
fi
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable rustdesk
|
systemctl enable rustdesk
|
||||||
systemctl start rustdesk
|
systemctl start rustdesk
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.2.6
|
pkgver=1.2.7
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
|
|||||||
@@ -91,11 +91,24 @@ def delete(url, token, guid, id):
|
|||||||
return check(response)
|
return check(response)
|
||||||
|
|
||||||
|
|
||||||
|
def assign(url, token, guid, id, type, value):
|
||||||
|
print("assign", id, type, value)
|
||||||
|
if type != "ab" and type != "strategy_name" and type != "user_name":
|
||||||
|
print("Invalid type, it must be 'ab', 'strategy_name' or 'user_name'")
|
||||||
|
return
|
||||||
|
data = {"type": type, "value": value}
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
response = requests.post(
|
||||||
|
f"{url}/api/devices/{guid}/assign", headers=headers, json=data
|
||||||
|
)
|
||||||
|
return check(response)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Device manager")
|
parser = argparse.ArgumentParser(description="Device manager")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"command",
|
"command",
|
||||||
choices=["view", "disable", "enable", "delete"],
|
choices=["view", "disable", "enable", "delete", "assign"],
|
||||||
help="Command to execute",
|
help="Command to execute",
|
||||||
)
|
)
|
||||||
parser.add_argument("--url", required=True, help="URL of the API")
|
parser.add_argument("--url", required=True, help="URL of the API")
|
||||||
@@ -106,6 +119,10 @@ def main():
|
|||||||
parser.add_argument("--device_name", help="Device name")
|
parser.add_argument("--device_name", help="Device name")
|
||||||
parser.add_argument("--user_name", help="User name")
|
parser.add_argument("--user_name", help="User name")
|
||||||
parser.add_argument("--group_name", help="Group name")
|
parser.add_argument("--group_name", help="Group name")
|
||||||
|
parser.add_argument(
|
||||||
|
"--assign_to",
|
||||||
|
help="<type>=<value>, e.g. user_name=mike, strategy_name=test, ab=ab1, ab=ab1,tag1",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--offline_days", type=int, help="Offline duration in days, e.g., 7"
|
"--offline_days", type=int, help="Offline duration in days, e.g., 7"
|
||||||
)
|
)
|
||||||
@@ -137,6 +154,16 @@ def main():
|
|||||||
for device in devices:
|
for device in devices:
|
||||||
response = delete(args.url, args.token, device["guid"], device["id"])
|
response = delete(args.url, args.token, device["guid"], device["id"])
|
||||||
print(response)
|
print(response)
|
||||||
|
elif args.command == "assign":
|
||||||
|
if "=" not in args.assign_to:
|
||||||
|
print("Invalid assign_to format, it must be <type>=<value>")
|
||||||
|
return
|
||||||
|
type, value = args.assign_to.split("=", 1)
|
||||||
|
for device in devices:
|
||||||
|
response = assign(
|
||||||
|
args.url, args.token, device["guid"], device["id"], type, value
|
||||||
|
)
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
16
res/fdroid/patches/0000-flutter-android-x86.patch
Normal file
16
res/fdroid/patches/0000-flutter-android-x86.patch
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
diff --git a/flutter-sdk/.gclient b/flutter-sdk/.gclient
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..fd12886
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/flutter-sdk/.gclient
|
||||||
|
@@ -0,0 +1,10 @@
|
||||||
|
+solutions = [
|
||||||
|
+ {
|
||||||
|
+ "managed": False,
|
||||||
|
+ "name": "src/flutter",
|
||||||
|
+ "url": "https://github.com/flutter/engine.git@FLUTTER_VERSION_PLACEHOLDER",
|
||||||
|
+ "custom_deps": {},
|
||||||
|
+ "deps_file": "DEPS",
|
||||||
|
+ "safesync_url": "",
|
||||||
|
+ },
|
||||||
|
+]
|
||||||
24
res/fdroid/patches/0001-x86-no-debuggable.patch
Normal file
24
res/fdroid/patches/0001-x86-no-debuggable.patch
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle
|
||||||
|
index f4dc69e..6b835fd 100644
|
||||||
|
--- a/flutter/android/app/build.gradle
|
||||||
|
+++ b/flutter/android/app/build.gradle
|
||||||
|
@@ -67,6 +67,19 @@ android {
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+ applicationVariants.all { variant ->
|
||||||
|
+ variant.outputs.each { output ->
|
||||||
|
+ output.processManifest.doLast { task ->
|
||||||
|
+ def outputDir = multiApkManifestOutputDirectory.asFile.get()
|
||||||
|
+ File manifestOutFile = new File(outputDir, "AndroidManifest.xml")
|
||||||
|
+ if (manifestOutFile.exists()) {
|
||||||
|
+ def newFileContents = manifestOutFile.getText('UTF-8').replace("android:debuggable=\"true\"", "")
|
||||||
|
+ manifestOutFile.write(newFileContents, 'UTF-8')
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
@@ -33,10 +33,12 @@ g_arpsystemcomponent = {
|
|||||||
},
|
},
|
||||||
"ReadMe": {
|
"ReadMe": {
|
||||||
"msi": "ARPREADME",
|
"msi": "ARPREADME",
|
||||||
"v": "https://github.com/fufesou/rustdesk",
|
"v": "https://github.com/rustdesk/rustdesk",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def default_revision_version():
|
||||||
|
return int(datetime.datetime.now().timestamp() / 60)
|
||||||
|
|
||||||
def make_parser():
|
def make_parser():
|
||||||
parser = argparse.ArgumentParser(description="Msi preprocess script.")
|
parser = argparse.ArgumentParser(description="Msi preprocess script.")
|
||||||
@@ -68,6 +70,9 @@ def make_parser():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-v", "--version", type=str, default="", help="The app version."
|
"-v", "--version", type=str, default="", help="The app version."
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--revision-version", type=int, default=default_revision_version(), help="The revision version."
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-m",
|
"-m",
|
||||||
"--manufacturer",
|
"--manufacturer",
|
||||||
@@ -430,6 +435,11 @@ def init_global_vars(dist_dir, app_name, args):
|
|||||||
if not version_pattern.match(g_version):
|
if not version_pattern.match(g_version):
|
||||||
print(f"Error: version {g_version} not found in {dist_app}")
|
print(f"Error: version {g_version} not found in {dist_app}")
|
||||||
return False
|
return False
|
||||||
|
if g_version.count(".") == 2:
|
||||||
|
# https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/Version.cs
|
||||||
|
if args.revision_version < 0 or args.revision_version > 2147483647:
|
||||||
|
raise ValueError(f"Invalid revision version: {args.revision_version}")
|
||||||
|
g_version = f"{g_version}.{args.revision_version}"
|
||||||
|
|
||||||
g_build_date = read_process_output("--build-date")
|
g_build_date = read_process_output("--build-date")
|
||||||
build_date_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}")
|
build_date_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.2.6
|
Version: 1.2.7
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.2.6
|
Version: 1.2.7
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.2.6
|
Version: 1.2.7
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
||||||
index 8f459f39c4..d8c1bb2b02 100644
|
|
||||||
--- a/CMakeLists.txt
|
|
||||||
+++ b/CMakeLists.txt
|
|
||||||
@@ -286,12 +286,12 @@ add_library(aom ${target_objs_aom} $<TARGET_OBJECTS:aom_rtcd>)
|
|
||||||
|
|
||||||
if(BUILD_SHARED_LIBS)
|
|
||||||
add_library(aom_static STATIC ${target_objs_aom} $<TARGET_OBJECTS:aom_rtcd>)
|
|
||||||
- set_target_properties(aom_static PROPERTIES OUTPUT_NAME aom)
|
|
||||||
+ set_target_properties(aom_static PROPERTIES OUTPUT_NAME aom_static)
|
|
||||||
if(MSVC OR (WIN32 AND NOT MINGW))
|
|
||||||
# Fix race condition on the export library file between the two versions.
|
|
||||||
# Affects MSVC in all three flavors (stock, Clang/CL, LLVM-- the latter sets
|
|
||||||
# MSVC and MINGW both to FALSE).
|
|
||||||
- set_target_properties(aom PROPERTIES ARCHIVE_OUTPUT_NAME "aom_dll")
|
|
||||||
+ set_target_properties(aom PROPERTIES ARCHIVE_OUTPUT_NAME "aom")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT MSVC)
|
|
||||||
@@ -11,9 +11,8 @@ vcpkg_add_to_path(${PERL_PATH})
|
|||||||
vcpkg_from_git(
|
vcpkg_from_git(
|
||||||
OUT_SOURCE_PATH SOURCE_PATH
|
OUT_SOURCE_PATH SOURCE_PATH
|
||||||
URL "https://aomedia.googlesource.com/aom"
|
URL "https://aomedia.googlesource.com/aom"
|
||||||
REF 6054fae218eda6e53e1e3b4f7ef0fff4877c7bf1 # v3.7.0
|
REF 8ad484f8a18ed1853c094e7d3a4e023b2a92df28 # 3.9.1
|
||||||
PATCHES
|
PATCHES
|
||||||
aom-rename-static.diff
|
|
||||||
aom-uninitialized-pointer.diff
|
aom-uninitialized-pointer.diff
|
||||||
# Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream
|
# Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream
|
||||||
aom-install.diff
|
aom-install.diff
|
||||||
@@ -47,6 +46,13 @@ vcpkg_copy_pdbs()
|
|||||||
|
|
||||||
vcpkg_fixup_pkgconfig()
|
vcpkg_fixup_pkgconfig()
|
||||||
|
|
||||||
|
if(VCPKG_TARGET_IS_WINDOWS)
|
||||||
|
vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/aom.pc" " -lm" "")
|
||||||
|
if(NOT VCPKG_BUILD_TYPE)
|
||||||
|
vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/aom.pc" " -lm" "")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Move cmake configs
|
# Move cmake configs
|
||||||
vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/${PORT})
|
vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/${PORT})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aom",
|
"name": "aom",
|
||||||
"version-semver": "3.7.0",
|
"version-semver": "3.9.1",
|
||||||
|
"port-version": 0,
|
||||||
"description": "AV1 codec library",
|
"description": "AV1 codec library",
|
||||||
"homepage": "https://aomedia.googlesource.com/aom",
|
"homepage": "https://aomedia.googlesource.com/aom",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
|||||||
@@ -85,19 +85,19 @@ index 58bb66b..b4cad6c 100644
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
diff --git a/configure b/configure
|
diff --git a/configure b/configure
|
||||||
index ae289f7..78f5fc1 100644
|
index b212e07..1a9fa98 100755
|
||||||
--- a/configure
|
--- a/configure
|
||||||
+++ b/configure
|
+++ b/configure
|
||||||
@@ -103,6 +103,8 @@ all_platforms="${all_platforms} arm64-darwin20-gcc"
|
@@ -104,6 +104,8 @@ all_platforms="${all_platforms} arm64-darwin21-gcc"
|
||||||
all_platforms="${all_platforms} arm64-darwin21-gcc"
|
|
||||||
all_platforms="${all_platforms} arm64-darwin22-gcc"
|
all_platforms="${all_platforms} arm64-darwin22-gcc"
|
||||||
|
all_platforms="${all_platforms} arm64-darwin23-gcc"
|
||||||
all_platforms="${all_platforms} arm64-linux-gcc"
|
all_platforms="${all_platforms} arm64-linux-gcc"
|
||||||
+all_platforms="${all_platforms} arm64-uwp-vs16"
|
+all_platforms="${all_platforms} arm64-uwp-vs16"
|
||||||
+all_platforms="${all_platforms} arm64-uwp-vs17"
|
+all_platforms="${all_platforms} arm64-uwp-vs17"
|
||||||
all_platforms="${all_platforms} arm64-win64-gcc"
|
all_platforms="${all_platforms} arm64-win64-gcc"
|
||||||
all_platforms="${all_platforms} arm64-win64-vs15"
|
all_platforms="${all_platforms} arm64-win64-vs15"
|
||||||
all_platforms="${all_platforms} arm64-win64-vs16"
|
all_platforms="${all_platforms} arm64-win64-vs16"
|
||||||
@@ -112,6 +114,8 @@ all_platforms="${all_platforms} armv7-darwin-gcc" #neon Cortex-A8
|
@@ -115,6 +117,8 @@ all_platforms="${all_platforms} armv7-darwin-gcc" #neon Cortex-A8
|
||||||
all_platforms="${all_platforms} armv7-linux-rvct" #neon Cortex-A8
|
all_platforms="${all_platforms} armv7-linux-rvct" #neon Cortex-A8
|
||||||
all_platforms="${all_platforms} armv7-linux-gcc" #neon Cortex-A8
|
all_platforms="${all_platforms} armv7-linux-gcc" #neon Cortex-A8
|
||||||
all_platforms="${all_platforms} armv7-none-rvct" #neon Cortex-A8
|
all_platforms="${all_platforms} armv7-none-rvct" #neon Cortex-A8
|
||||||
@@ -106,7 +106,7 @@ index ae289f7..78f5fc1 100644
|
|||||||
all_platforms="${all_platforms} armv7-win32-gcc"
|
all_platforms="${all_platforms} armv7-win32-gcc"
|
||||||
all_platforms="${all_platforms} armv7-win32-vs14"
|
all_platforms="${all_platforms} armv7-win32-vs14"
|
||||||
all_platforms="${all_platforms} armv7-win32-vs15"
|
all_platforms="${all_platforms} armv7-win32-vs15"
|
||||||
@@ -143,6 +147,8 @@ all_platforms="${all_platforms} x86-linux-gcc"
|
@@ -146,6 +150,8 @@ all_platforms="${all_platforms} x86-linux-gcc"
|
||||||
all_platforms="${all_platforms} x86-linux-icc"
|
all_platforms="${all_platforms} x86-linux-icc"
|
||||||
all_platforms="${all_platforms} x86-os2-gcc"
|
all_platforms="${all_platforms} x86-os2-gcc"
|
||||||
all_platforms="${all_platforms} x86-solaris-gcc"
|
all_platforms="${all_platforms} x86-solaris-gcc"
|
||||||
@@ -115,7 +115,7 @@ index ae289f7..78f5fc1 100644
|
|||||||
all_platforms="${all_platforms} x86-win32-gcc"
|
all_platforms="${all_platforms} x86-win32-gcc"
|
||||||
all_platforms="${all_platforms} x86-win32-vs14"
|
all_platforms="${all_platforms} x86-win32-vs14"
|
||||||
all_platforms="${all_platforms} x86-win32-vs15"
|
all_platforms="${all_platforms} x86-win32-vs15"
|
||||||
@@ -167,6 +173,8 @@ all_platforms="${all_platforms} x86_64-iphonesimulator-gcc"
|
@@ -171,6 +177,8 @@ all_platforms="${all_platforms} x86_64-iphonesimulator-gcc"
|
||||||
all_platforms="${all_platforms} x86_64-linux-gcc"
|
all_platforms="${all_platforms} x86_64-linux-gcc"
|
||||||
all_platforms="${all_platforms} x86_64-linux-icc"
|
all_platforms="${all_platforms} x86_64-linux-icc"
|
||||||
all_platforms="${all_platforms} x86_64-solaris-gcc"
|
all_platforms="${all_platforms} x86_64-solaris-gcc"
|
||||||
@@ -124,7 +124,7 @@ index ae289f7..78f5fc1 100644
|
|||||||
all_platforms="${all_platforms} x86_64-win64-gcc"
|
all_platforms="${all_platforms} x86_64-win64-gcc"
|
||||||
all_platforms="${all_platforms} x86_64-win64-vs14"
|
all_platforms="${all_platforms} x86_64-win64-vs14"
|
||||||
all_platforms="${all_platforms} x86_64-win64-vs15"
|
all_platforms="${all_platforms} x86_64-win64-vs15"
|
||||||
@@ -491,11 +499,10 @@ process_targets() {
|
@@ -503,11 +511,10 @@ process_targets() {
|
||||||
! enabled multithread && DIST_DIR="${DIST_DIR}-nomt"
|
! enabled multithread && DIST_DIR="${DIST_DIR}-nomt"
|
||||||
! enabled install_docs && DIST_DIR="${DIST_DIR}-nodocs"
|
! enabled install_docs && DIST_DIR="${DIST_DIR}-nodocs"
|
||||||
DIST_DIR="${DIST_DIR}-${tgt_isa}-${tgt_os}"
|
DIST_DIR="${DIST_DIR}-${tgt_isa}-${tgt_os}"
|
||||||
@@ -140,7 +140,7 @@ index ae289f7..78f5fc1 100644
|
|||||||
if [ -f "${source_path}/build/make/version.sh" ]; then
|
if [ -f "${source_path}/build/make/version.sh" ]; then
|
||||||
ver=`"$source_path/build/make/version.sh" --bare "$source_path"`
|
ver=`"$source_path/build/make/version.sh" --bare "$source_path"`
|
||||||
DIST_DIR="${DIST_DIR}-${ver}"
|
DIST_DIR="${DIST_DIR}-${ver}"
|
||||||
@@ -584,6 +591,10 @@ process_detect() {
|
@@ -596,6 +603,10 @@ process_detect() {
|
||||||
|
|
||||||
# Specialize windows and POSIX environments.
|
# Specialize windows and POSIX environments.
|
||||||
case $toolchain in
|
case $toolchain in
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
diff --git a/vp9/encoder/arm/neon/vp9_diamond_search_sad_neon.c b/vp9/encoder/arm/neon/vp9_diamond_search_sad_neon.c
|
|
||||||
index 33753f7..997775a 100644
|
|
||||||
--- a/vp9/encoder/arm/neon/vp9_diamond_search_sad_neon.c
|
|
||||||
+++ b/vp9/encoder/arm/neon/vp9_diamond_search_sad_neon.c
|
|
||||||
@@ -220,7 +220,7 @@ int vp9_diamond_search_sad_neon(const MACROBLOCK *x,
|
|
||||||
// Look up the component cost of the residual motion vector
|
|
||||||
{
|
|
||||||
uint32_t cost[4];
|
|
||||||
- int16_t __attribute__((aligned(16))) rowcol[8];
|
|
||||||
+ DECLARE_ALIGNED(16, int16_t, rowcol[8]);
|
|
||||||
vst1q_s16(rowcol, v_diff_mv_w);
|
|
||||||
|
|
||||||
// Note: This is a use case for gather instruction
|
|
||||||
@@ -4,28 +4,25 @@ vcpkg_from_github(
|
|||||||
OUT_SOURCE_PATH SOURCE_PATH
|
OUT_SOURCE_PATH SOURCE_PATH
|
||||||
REPO webmproject/libvpx
|
REPO webmproject/libvpx
|
||||||
REF "v${VERSION}"
|
REF "v${VERSION}"
|
||||||
SHA512 49706838563c92fab7334376848d0f374efcbc1729ef511e967c908fd2ecd40e8d197f1d85da6553b3a7026bdbc17e5a76595319858af26ce58cb9a4c3854897
|
SHA512 3e3bfad3d035c0bc3db7cb5a194d56d3c90f5963fb1ad527ae5252054e7c48ce2973de1346c97d94b59f7a95d4801bec44214cce10faf123f92b36fca79a8d1e
|
||||||
HEAD_REF master
|
HEAD_REF master
|
||||||
PATCHES
|
PATCHES
|
||||||
0002-Fix-nasm-debug-format-flag.patch
|
0002-Fix-nasm-debug-format-flag.patch
|
||||||
0003-add-uwp-v142-and-v143-support.patch
|
0003-add-uwp-v142-and-v143-support.patch
|
||||||
0004-remove-library-suffixes.patch
|
0004-remove-library-suffixes.patch
|
||||||
0005-fix-arm64-build.patch # Upstream commit: https://github.com/webmproject/libvpx/commit/858a8c611f4c965078485860a6820e2135e6611b
|
|
||||||
)
|
)
|
||||||
|
|
||||||
vcpkg_find_acquire_program(PERL)
|
|
||||||
|
|
||||||
get_filename_component(PERL_EXE_PATH ${PERL} DIRECTORY)
|
|
||||||
|
|
||||||
if(CMAKE_HOST_WIN32)
|
if(CMAKE_HOST_WIN32)
|
||||||
vcpkg_acquire_msys(MSYS_ROOT PACKAGES make)
|
vcpkg_acquire_msys(MSYS_ROOT PACKAGES make perl)
|
||||||
set(BASH ${MSYS_ROOT}/usr/bin/bash.exe)
|
set(ENV{PATH} "${MSYS_ROOT}/usr/bin;$ENV{PATH}")
|
||||||
set(ENV{PATH} "${MSYS_ROOT}/usr/bin;$ENV{PATH};${PERL_EXE_PATH}")
|
|
||||||
else()
|
else()
|
||||||
set(BASH /bin/bash)
|
vcpkg_find_acquire_program(PERL)
|
||||||
|
get_filename_component(PERL_EXE_PATH ${PERL} DIRECTORY)
|
||||||
set(ENV{PATH} "${MSYS_ROOT}/usr/bin:$ENV{PATH}:${PERL_EXE_PATH}")
|
set(ENV{PATH} "${MSYS_ROOT}/usr/bin:$ENV{PATH}:${PERL_EXE_PATH}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
find_program(BASH NAME bash HINTS ${MSYS_ROOT}/usr/bin REQUIRED NO_CACHE)
|
||||||
|
|
||||||
vcpkg_find_acquire_program(NASM)
|
vcpkg_find_acquire_program(NASM)
|
||||||
get_filename_component(NASM_EXE_PATH ${NASM} DIRECTORY)
|
get_filename_component(NASM_EXE_PATH ${NASM} DIRECTORY)
|
||||||
vcpkg_add_to_path(${NASM_EXE_PATH})
|
vcpkg_add_to_path(${NASM_EXE_PATH})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "libvpx",
|
"name": "libvpx",
|
||||||
"version": "1.13.1",
|
"version": "1.14.1",
|
||||||
|
"port-version": 0,
|
||||||
"description": "The reference software implementation for the video coding formats VP8 and VP9.",
|
"description": "The reference software implementation for the video coding formats VP8 and VP9.",
|
||||||
"homepage": "https://github.com/webmproject/libvpx",
|
"homepage": "https://github.com/webmproject/libvpx",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vcpkg-msbuild",
|
"name": "vcpkg-msbuild",
|
||||||
"host": true,
|
"host": true,
|
||||||
"platform": "windows"
|
"platform": "windows & !mingw"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"features": {
|
"features": {
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ vcpkg_from_github(
|
|||||||
OUT_SOURCE_PATH SOURCE_PATH
|
OUT_SOURCE_PATH SOURCE_PATH
|
||||||
REPO xiph/opus
|
REPO xiph/opus
|
||||||
REF "v${VERSION}"
|
REF "v${VERSION}"
|
||||||
SHA512 86df35cd62ebf3551b2739effb8f818d635656d91d386d7d600a424a92c4c0d6bfbc3986f1ec6cf4950910ac87b28dc9640b9df3b9a6a5a75eb37ae71782b72e
|
SHA512 ba79ad035993e7bc4c09b7d77964ba913eb0b2be33305e8a04a8c49aaab21c4d96ac828e31ae45484896105851fdfc8c305c63c8400e4481dd76c62a1c12286b
|
||||||
HEAD_REF master
|
HEAD_REF main
|
||||||
PATCHES fix-pkgconfig-version.patch
|
PATCHES fix-pkgconfig-version.patch
|
||||||
)
|
)
|
||||||
|
|
||||||
vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS
|
vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS
|
||||||
FEATURES
|
FEATURES
|
||||||
avx AVX_SUPPORTED
|
avx2 AVX2_SUPPORTED
|
||||||
)
|
)
|
||||||
|
|
||||||
set(ADDITIONAL_OPUS_OPTIONS "")
|
set(ADDITIONAL_OPUS_OPTIONS "")
|
||||||
@@ -17,14 +17,17 @@ if(VCPKG_TARGET_IS_MINGW)
|
|||||||
set(STACK_PROTECTOR OFF)
|
set(STACK_PROTECTOR OFF)
|
||||||
string(APPEND VCPKG_C_FLAGS "-D_FORTIFY_SOURCE=0")
|
string(APPEND VCPKG_C_FLAGS "-D_FORTIFY_SOURCE=0")
|
||||||
string(APPEND VCPKG_CXX_FLAGS "-D_FORTIFY_SOURCE=0")
|
string(APPEND VCPKG_CXX_FLAGS "-D_FORTIFY_SOURCE=0")
|
||||||
|
if(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)64$")
|
||||||
|
list(APPEND ADDITIONAL_OPUS_OPTIONS "-DOPUS_USE_NEON=OFF") # for version 1.3.1 (remove for future Opus release)
|
||||||
|
list(APPEND ADDITIONAL_OPUS_OPTIONS "-DOPUS_DISABLE_INTRINSICS=ON") # for HEAD (and future Opus release)
|
||||||
|
endif()
|
||||||
elseif(VCPKG_TARGET_IS_EMSCRIPTEN)
|
elseif(VCPKG_TARGET_IS_EMSCRIPTEN)
|
||||||
set(STACK_PROTECTOR OFF)
|
set(STACK_PROTECTOR OFF)
|
||||||
else()
|
else()
|
||||||
set(STACK_PROTECTOR ON)
|
set(STACK_PROTECTOR ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if((VCPKG_TARGET_IS_MINGW AND VCPKG_TARGET_ARCHITECTURE MATCHES "^arm") OR
|
if((VCPKG_TARGET_IS_LINUX AND VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") OR
|
||||||
(VCPKG_TARGET_IS_LINUX AND VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") OR
|
|
||||||
(VCPKG_TARGET_IS_ANDROID AND VCPKG_TARGET_ARCHITECTURE STREQUAL "arm" AND VCPKG_CMAKE_CONFIGURE_OPTIONS MATCHES "ANDROID_ARM_NEON"))
|
(VCPKG_TARGET_IS_ANDROID AND VCPKG_TARGET_ARCHITECTURE STREQUAL "arm" AND VCPKG_CMAKE_CONFIGURE_OPTIONS MATCHES "ANDROID_ARM_NEON"))
|
||||||
message(STATUS "Disabling ARM NEON and intrinsics on ${TARGET_TRIPLET}")
|
message(STATUS "Disabling ARM NEON and intrinsics on ${TARGET_TRIPLET}")
|
||||||
list(APPEND ADDITIONAL_OPUS_OPTIONS "-DOPUS_DISABLE_INTRINSICS=ON -DCOMPILER_SUPPORTS_NEON=OFF") # for HEAD (and future Opus release)
|
list(APPEND ADDITIONAL_OPUS_OPTIONS "-DOPUS_DISABLE_INTRINSICS=ON -DCOMPILER_SUPPORTS_NEON=OFF") # for HEAD (and future Opus release)
|
||||||
@@ -55,4 +58,4 @@ file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/lib/cmake"
|
|||||||
"${CURRENT_PACKAGES_DIR}/lib/cmake"
|
"${CURRENT_PACKAGES_DIR}/lib/cmake"
|
||||||
"${CURRENT_PACKAGES_DIR}/debug/include")
|
"${CURRENT_PACKAGES_DIR}/debug/include")
|
||||||
|
|
||||||
file(INSTALL "${SOURCE_PATH}/COPYING" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright)
|
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/COPYING")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opus",
|
"name": "opus",
|
||||||
"version": "1.4",
|
"version": "1.5.1",
|
||||||
"port-version": 1,
|
|
||||||
"description": "Totally open, royalty-free, highly versatile audio codec",
|
"description": "Totally open, royalty-free, highly versatile audio codec",
|
||||||
"homepage": "https://github.com/xiph/opus",
|
"homepage": "https://github.com/xiph/opus",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
@@ -16,8 +15,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"features": {
|
"features": {
|
||||||
"avx": {
|
"avx2": {
|
||||||
"description": "Builds the library with avx instruction set"
|
"description": "Builds the library with avx2 instruction set"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
|
anyhow::anyhow,
|
||||||
bail,
|
bail,
|
||||||
config::Config,
|
config::Config,
|
||||||
get_time,
|
get_time,
|
||||||
password_security::{decrypt_vec_or_original, encrypt_vec_or_original},
|
password_security::{decrypt_vec_or_original, encrypt_vec_or_original},
|
||||||
ResultType,
|
tokio, ResultType,
|
||||||
};
|
};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -109,3 +110,97 @@ pub fn get_2fa(raw: Option<String>) -> Option<TOTP> {
|
|||||||
.map(|x| Some(x))
|
.map(|x| Some(x))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct TelegramBot {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub token_str: String,
|
||||||
|
pub token: Vec<u8>,
|
||||||
|
pub chat_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TelegramBot {
|
||||||
|
fn into_string(&self) -> ResultType<String> {
|
||||||
|
let token = encrypt_vec_or_original(self.token_str.as_bytes(), "00", 1024);
|
||||||
|
let bot = TelegramBot {
|
||||||
|
token,
|
||||||
|
..self.clone()
|
||||||
|
};
|
||||||
|
let s = serde_json::to_string(&bot)?;
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> ResultType<()> {
|
||||||
|
let s = self.into_string()?;
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
crate::ipc::set_option("bot", &s);
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
Config::set_option("bot".to_owned(), s);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get() -> ResultType<Option<TelegramBot>> {
|
||||||
|
let data = Config::get_option("bot");
|
||||||
|
if data.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut bot = serde_json::from_str::<TelegramBot>(&data)?;
|
||||||
|
let (token, success, _) = decrypt_vec_or_original(&bot.token, "00");
|
||||||
|
if success {
|
||||||
|
bot.token_str = String::from_utf8(token)?;
|
||||||
|
return Ok(Some(bot));
|
||||||
|
}
|
||||||
|
bail!("decrypt_vec_or_original telegram bot token failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gist.github.com/dideler/85de4d64f66c1966788c1b2304b9caf1
|
||||||
|
pub async fn send_2fa_code_to_telegram(text: &str, bot: TelegramBot) -> ResultType<()> {
|
||||||
|
let url = format!("https://api.telegram.org/bot{}/sendMessage", bot.token_str);
|
||||||
|
let params = serde_json::json!({"chat_id": bot.chat_id, "text": text});
|
||||||
|
crate::post_request(url, params.to_string(), "").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_chatid_telegram(bot_token: &str) -> ResultType<Option<String>> {
|
||||||
|
let url = format!("https://api.telegram.org/bot{}/getUpdates", bot_token);
|
||||||
|
// because caller is in tokio runtime, so we must call post_request_sync in new thread.
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
crate::post_request_sync(url, "".to_owned(), "")
|
||||||
|
});
|
||||||
|
let resp = handle.join().map_err(|_| anyhow!("Thread panicked"))??;
|
||||||
|
let value = serde_json::from_str::<serde_json::Value>(&resp).map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
|
// Check for an error_code in the response
|
||||||
|
if let Some(error_code) = value.get("error_code").and_then(|code| code.as_i64()) {
|
||||||
|
// If there's an error_code, try to use the description for the error message
|
||||||
|
let description = value["description"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("Unknown error occurred");
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Telegram API error: {} (error_code: {})",
|
||||||
|
description,
|
||||||
|
error_code
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let chat_id = &value["result"][0]["message"]["chat"]["id"];
|
||||||
|
let chat_id = if let Some(id) = chat_id.as_i64() {
|
||||||
|
Some(id.to_string())
|
||||||
|
} else if let Some(id) = chat_id.as_str() {
|
||||||
|
Some(id.to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(chat_id) = chat_id.as_ref() {
|
||||||
|
let bot = TelegramBot {
|
||||||
|
token_str: bot_token.to_owned(),
|
||||||
|
chat_id: chat_id.to_owned(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
bot.save()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(chat_id)
|
||||||
|
}
|
||||||
|
|||||||
289
src/client.rs
289
src/client.rs
@@ -22,12 +22,9 @@ use sha2::{Digest, Sha256};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub use file_trait::FileManager;
|
pub use file_trait::FileManager;
|
||||||
#[cfg(windows)]
|
|
||||||
use hbb_common::tokio;
|
|
||||||
#[cfg(not(feature = "flutter"))]
|
#[cfg(not(feature = "flutter"))]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
use hbb_common::tokio::sync::mpsc::UnboundedSender;
|
use hbb_common::tokio::sync::mpsc::UnboundedSender;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
||||||
use hbb_common::tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
use hbb_common::tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
allow_err,
|
allow_err,
|
||||||
@@ -42,12 +39,14 @@ use hbb_common::{
|
|||||||
protobuf::{Message as _, MessageField},
|
protobuf::{Message as _, MessageField},
|
||||||
rand,
|
rand,
|
||||||
rendezvous_proto::*,
|
rendezvous_proto::*,
|
||||||
socket_client,
|
socket_client::{connect_tcp, connect_tcp_local, ipv4_to_ipv6},
|
||||||
sodiumoxide::base64,
|
sodiumoxide::{base64, crypto::sign},
|
||||||
sodiumoxide::crypto::sign,
|
|
||||||
tcp::FramedStream,
|
tcp::FramedStream,
|
||||||
timeout,
|
timeout,
|
||||||
tokio::time::Duration,
|
tokio::{
|
||||||
|
self,
|
||||||
|
time::{interval, Duration, Instant},
|
||||||
|
},
|
||||||
AddrMangle, ResultType, Stream,
|
AddrMangle, ResultType, Stream,
|
||||||
};
|
};
|
||||||
pub use helper::*;
|
pub use helper::*;
|
||||||
@@ -61,15 +60,15 @@ use crate::{
|
|||||||
check_port,
|
check_port,
|
||||||
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
|
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
|
||||||
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp,
|
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp,
|
||||||
ui_interface::use_texture_render,
|
ui_interface::{get_buildin_option, use_texture_render},
|
||||||
ui_session_interface::{InvokeUiSession, Session},
|
ui_session_interface::{InvokeUiSession, Session},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
use crate::clipboard::{check_clipboard, CLIPBOARD_INTERVAL};
|
||||||
#[cfg(not(feature = "flutter"))]
|
#[cfg(not(feature = "flutter"))]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
use crate::ui_session_interface::SessionPermissionConfig;
|
use crate::ui_session_interface::SessionPermissionConfig;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
||||||
use crate::{check_clipboard, CLIPBOARD_INTERVAL};
|
|
||||||
|
|
||||||
pub use super::lang::*;
|
pub use super::lang::*;
|
||||||
|
|
||||||
@@ -137,7 +136,7 @@ lazy_static::lazy_static! {
|
|||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref ENIGO: Arc<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
|
static ref ENIGO: Arc<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
|
||||||
static ref OLD_CLIPBOARD_TEXT: Arc<Mutex<String>> = Default::default();
|
static ref OLD_CLIPBOARD_DATA: Arc<Mutex<crate::clipboard::ClipboardData>> = Default::default();
|
||||||
static ref TEXT_CLIPBOARD_STATE: Arc<Mutex<TextClipboardState>> = Arc::new(Mutex::new(TextClipboardState::new()));
|
static ref TEXT_CLIPBOARD_STATE: Arc<Mutex<TextClipboardState>> = Arc::new(Mutex::new(TextClipboardState::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +144,8 @@ const PUBLIC_SERVER: &str = "public";
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn get_old_clipboard_text() -> &'static Arc<Mutex<String>> {
|
pub fn get_old_clipboard_text() -> Arc<Mutex<crate::clipboard::ClipboardData>> {
|
||||||
&OLD_CLIPBOARD_TEXT
|
OLD_CLIPBOARD_DATA.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
@@ -227,7 +226,7 @@ impl Client {
|
|||||||
token: &str,
|
token: &str,
|
||||||
conn_type: ConnType,
|
conn_type: ConnType,
|
||||||
interface: impl Interface,
|
interface: impl Interface,
|
||||||
) -> ResultType<(Stream, bool, Option<Vec<u8>>)> {
|
) -> ResultType<((Stream, bool, Option<Vec<u8>>), (i32, String))> {
|
||||||
debug_assert!(peer == interface.get_id());
|
debug_assert!(peer == interface.get_id());
|
||||||
interface.update_direct(None);
|
interface.update_direct(None);
|
||||||
interface.update_received(false);
|
interface.update_received(false);
|
||||||
@@ -251,25 +250,26 @@ impl Client {
|
|||||||
token: &str,
|
token: &str,
|
||||||
conn_type: ConnType,
|
conn_type: ConnType,
|
||||||
interface: impl Interface,
|
interface: impl Interface,
|
||||||
) -> ResultType<(Stream, bool, Option<Vec<u8>>)> {
|
) -> ResultType<((Stream, bool, Option<Vec<u8>>), (i32, String))> {
|
||||||
if config::is_incoming_only() {
|
if config::is_incoming_only() {
|
||||||
bail!("Incoming only mode");
|
bail!("Incoming only mode");
|
||||||
}
|
}
|
||||||
// to-do: remember the port for each peer, so that we can retry easier
|
// to-do: remember the port for each peer, so that we can retry easier
|
||||||
if hbb_common::is_ip_str(peer) {
|
if hbb_common::is_ip_str(peer) {
|
||||||
return Ok((
|
return Ok((
|
||||||
socket_client::connect_tcp(check_port(peer, RELAY_PORT + 1), CONNECT_TIMEOUT)
|
(
|
||||||
.await?,
|
connect_tcp(check_port(peer, RELAY_PORT + 1), CONNECT_TIMEOUT).await?,
|
||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
|
),
|
||||||
|
(0, "".to_owned()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Allow connect to {domain}:{port}
|
// Allow connect to {domain}:{port}
|
||||||
if hbb_common::is_domain_port_str(peer) {
|
if hbb_common::is_domain_port_str(peer) {
|
||||||
return Ok((
|
return Ok((
|
||||||
socket_client::connect_tcp(peer, CONNECT_TIMEOUT).await?,
|
(connect_tcp(peer, CONNECT_TIMEOUT).await?, true, None),
|
||||||
true,
|
(0, "".to_owned()),
|
||||||
None,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,13 +296,13 @@ impl Client {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut socket = socket_client::connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await;
|
let mut socket = connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await;
|
||||||
debug_assert!(!servers.contains(&rendezvous_server));
|
debug_assert!(!servers.contains(&rendezvous_server));
|
||||||
if socket.is_err() && !servers.is_empty() {
|
if socket.is_err() && !servers.is_empty() {
|
||||||
log::info!("try the other servers: {:?}", servers);
|
log::info!("try the other servers: {:?}", servers);
|
||||||
for server in servers {
|
for server in servers {
|
||||||
let server = check_port(server, RENDEZVOUS_PORT);
|
let server = check_port(server, RENDEZVOUS_PORT);
|
||||||
socket = socket_client::connect_tcp(&*server, CONNECT_TIMEOUT).await;
|
socket = connect_tcp(&*server, CONNECT_TIMEOUT).await;
|
||||||
if socket.is_ok() {
|
if socket.is_ok() {
|
||||||
rendezvous_server = server;
|
rendezvous_server = server;
|
||||||
break;
|
break;
|
||||||
@@ -328,6 +328,7 @@ impl Client {
|
|||||||
let mut peer_nat_type = NatType::UNKNOWN_NAT;
|
let mut peer_nat_type = NatType::UNKNOWN_NAT;
|
||||||
let my_nat_type = crate::get_nat_type(100).await;
|
let my_nat_type = crate::get_nat_type(100).await;
|
||||||
let mut is_local = false;
|
let mut is_local = false;
|
||||||
|
let mut feedback = 0;
|
||||||
for i in 1..=3 {
|
for i in 1..=3 {
|
||||||
log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer);
|
log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer);
|
||||||
let mut msg_out = RendezvousMessage::new();
|
let mut msg_out = RendezvousMessage::new();
|
||||||
@@ -343,9 +344,11 @@ impl Client {
|
|||||||
nat_type: nat_type.into(),
|
nat_type: nat_type.into(),
|
||||||
licence_key: key.to_owned(),
|
licence_key: key.to_owned(),
|
||||||
conn_type: conn_type.into(),
|
conn_type: conn_type.into(),
|
||||||
|
version: crate::VERSION.to_owned(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
socket.send(&msg_out).await?;
|
socket.send(&msg_out).await?;
|
||||||
|
// below timeout should not bigger than hbbs's connection timeout.
|
||||||
if let Some(msg_in) =
|
if let Some(msg_in) =
|
||||||
crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 6000)).await
|
crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 6000)).await
|
||||||
{
|
{
|
||||||
@@ -376,6 +379,7 @@ impl Client {
|
|||||||
signed_id_pk = ph.pk.into();
|
signed_id_pk = ph.pk.into();
|
||||||
relay_server = ph.relay_server;
|
relay_server = ph.relay_server;
|
||||||
peer_addr = AddrMangle::decode(&ph.socket_addr);
|
peer_addr = AddrMangle::decode(&ph.socket_addr);
|
||||||
|
feedback = ph.feedback;
|
||||||
log::info!("Hole Punched {} = {}", peer, peer_addr);
|
log::info!("Hole Punched {} = {}", peer, peer_addr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -396,9 +400,10 @@ impl Client {
|
|||||||
my_addr.is_ipv4(),
|
my_addr.is_ipv4(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
feedback = rr.feedback;
|
||||||
let pk =
|
let pk =
|
||||||
Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?;
|
Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?;
|
||||||
return Ok((conn, false, pk));
|
return Ok(((conn, false, pk), (feedback, rendezvous_server)));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("Unexpected protobuf msg received: {:?}", msg_in);
|
log::error!("Unexpected protobuf msg received: {:?}", msg_in);
|
||||||
@@ -421,23 +426,26 @@ impl Client {
|
|||||||
format!("nat_type: {:?}", peer_nat_type)
|
format!("nat_type: {:?}", peer_nat_type)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Self::connect(
|
Ok((
|
||||||
my_addr,
|
Self::connect(
|
||||||
peer_addr,
|
my_addr,
|
||||||
peer,
|
peer_addr,
|
||||||
signed_id_pk,
|
peer,
|
||||||
&relay_server,
|
signed_id_pk,
|
||||||
&rendezvous_server,
|
&relay_server,
|
||||||
time_used,
|
&rendezvous_server,
|
||||||
peer_nat_type,
|
time_used,
|
||||||
my_nat_type,
|
peer_nat_type,
|
||||||
is_local,
|
my_nat_type,
|
||||||
key,
|
is_local,
|
||||||
token,
|
key,
|
||||||
conn_type,
|
token,
|
||||||
interface,
|
conn_type,
|
||||||
)
|
interface,
|
||||||
.await
|
)
|
||||||
|
.await?,
|
||||||
|
(feedback, rendezvous_server),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to the peer.
|
/// Connect to the peer.
|
||||||
@@ -492,8 +500,7 @@ impl Client {
|
|||||||
log::info!("peer address: {}, timeout: {}", peer, connect_timeout);
|
log::info!("peer address: {}, timeout: {}", peer, connect_timeout);
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
// NOTICE: Socks5 is be used event in intranet. Which may be not a good way.
|
// NOTICE: Socks5 is be used event in intranet. Which may be not a good way.
|
||||||
let mut conn =
|
let mut conn = connect_tcp_local(peer, Some(local_addr), connect_timeout).await;
|
||||||
socket_client::connect_tcp_local(peer, Some(local_addr), connect_timeout).await;
|
|
||||||
let mut direct = !conn.is_err();
|
let mut direct = !conn.is_err();
|
||||||
interface.update_direct(Some(direct));
|
interface.update_direct(Some(direct));
|
||||||
if interface.is_force_relay() || conn.is_err() {
|
if interface.is_force_relay() || conn.is_err() {
|
||||||
@@ -536,7 +543,7 @@ impl Client {
|
|||||||
conn: &mut Stream,
|
conn: &mut Stream,
|
||||||
) -> ResultType<Option<Vec<u8>>> {
|
) -> ResultType<Option<Vec<u8>>> {
|
||||||
let rs_pk = get_rs_pk(if key.is_empty() {
|
let rs_pk = get_rs_pk(if key.is_empty() {
|
||||||
hbb_common::config::RS_PUB_KEY
|
config::RS_PUB_KEY
|
||||||
} else {
|
} else {
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
@@ -623,7 +630,7 @@ impl Client {
|
|||||||
|
|
||||||
for i in 1..=3 {
|
for i in 1..=3 {
|
||||||
// use different socket due to current hbbs implementation requiring different nat address for each attempt
|
// use different socket due to current hbbs implementation requiring different nat address for each attempt
|
||||||
let mut socket = socket_client::connect_tcp(rendezvous_server, CONNECT_TIMEOUT)
|
let mut socket = connect_tcp(rendezvous_server, CONNECT_TIMEOUT)
|
||||||
.await
|
.await
|
||||||
.with_context(|| "Failed to connect to rendezvous server")?;
|
.with_context(|| "Failed to connect to rendezvous server")?;
|
||||||
|
|
||||||
@@ -680,8 +687,8 @@ impl Client {
|
|||||||
conn_type: ConnType,
|
conn_type: ConnType,
|
||||||
ipv4: bool,
|
ipv4: bool,
|
||||||
) -> ResultType<Stream> {
|
) -> ResultType<Stream> {
|
||||||
let mut conn = socket_client::connect_tcp(
|
let mut conn = connect_tcp(
|
||||||
socket_client::ipv4_to_ipv6(check_port(relay_server, RELAY_PORT), ipv4),
|
ipv4_to_ipv6(check_port(relay_server, RELAY_PORT), ipv4),
|
||||||
CONNECT_TIMEOUT,
|
CONNECT_TIMEOUT,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -737,10 +744,11 @@ impl Client {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required {
|
if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required {
|
||||||
|
std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(msg) = check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)) {
|
if let Some(msg) = check_clipboard(&mut ctx, Some(OLD_CLIPBOARD_DATA.clone())) {
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
crate::flutter::send_text_clipboard_msg(msg);
|
crate::flutter::send_text_clipboard_msg(msg);
|
||||||
#[cfg(not(feature = "flutter"))]
|
#[cfg(not(feature = "flutter"))]
|
||||||
@@ -766,12 +774,12 @@ impl Client {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
fn get_current_text_clipboard_msg() -> Option<Message> {
|
fn get_current_clipboard_msg() -> Option<Message> {
|
||||||
let txt = &*OLD_CLIPBOARD_TEXT.lock().unwrap();
|
let data = &*OLD_CLIPBOARD_DATA.lock().unwrap();
|
||||||
if txt.is_empty() {
|
if data.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(crate::create_clipboard_msg(txt.clone()))
|
Some(data.create_msg())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1204,6 +1212,7 @@ pub struct LoginConfigHandler {
|
|||||||
pub save_ab_password_to_recent: bool, // true: connected with ab password
|
pub save_ab_password_to_recent: bool, // true: connected with ab password
|
||||||
pub other_server: Option<(String, String, String)>,
|
pub other_server: Option<(String, String, String)>,
|
||||||
pub custom_fps: Arc<Mutex<Option<usize>>>,
|
pub custom_fps: Arc<Mutex<Option<usize>>>,
|
||||||
|
pub last_auto_fps: Option<usize>,
|
||||||
pub adapter_luid: Option<i64>,
|
pub adapter_luid: Option<i64>,
|
||||||
pub mark_unsupported: Vec<CodecFormat>,
|
pub mark_unsupported: Vec<CodecFormat>,
|
||||||
pub selected_windows_session_id: Option<u32>,
|
pub selected_windows_session_id: Option<u32>,
|
||||||
@@ -2018,11 +2027,15 @@ impl LoginConfigHandler {
|
|||||||
} else {
|
} else {
|
||||||
(my_id, self.id.clone())
|
(my_id, self.id.clone())
|
||||||
};
|
};
|
||||||
|
let mut display_name = get_buildin_option(config::keys::OPTION_DISPLAY_NAME);
|
||||||
|
if display_name.is_empty() {
|
||||||
|
display_name = crate::username();
|
||||||
|
}
|
||||||
let mut lr = LoginRequest {
|
let mut lr = LoginRequest {
|
||||||
username: pure_id,
|
username: pure_id,
|
||||||
password: password.into(),
|
password: password.into(),
|
||||||
my_id,
|
my_id,
|
||||||
my_name: crate::username(),
|
my_name: display_name,
|
||||||
option: self.get_option_message(true).into(),
|
option: self.get_option_message(true).into(),
|
||||||
session_id: self.session_id,
|
session_id: self.session_id,
|
||||||
version: crate::VERSION.to_string(),
|
version: crate::VERSION.to_string(),
|
||||||
@@ -2093,8 +2106,6 @@ pub type MediaSender = mpsc::Sender<MediaData>;
|
|||||||
|
|
||||||
struct VideoHandlerController {
|
struct VideoHandlerController {
|
||||||
handler: VideoHandler,
|
handler: VideoHandler,
|
||||||
count: u128,
|
|
||||||
duration: std::time::Duration,
|
|
||||||
skip_beginning: u32,
|
skip_beginning: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2111,7 +2122,7 @@ pub fn start_video_audio_threads<F, T>(
|
|||||||
MediaSender,
|
MediaSender,
|
||||||
MediaSender,
|
MediaSender,
|
||||||
Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
|
Arc<RwLock<HashMap<usize, ArrayQueue<VideoFrame>>>>,
|
||||||
Arc<RwLock<HashMap<usize, usize>>>,
|
Arc<RwLock<Option<usize>>>,
|
||||||
Arc<RwLock<Option<Chroma>>>,
|
Arc<RwLock<Option<Chroma>>>,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
@@ -2123,8 +2134,8 @@ where
|
|||||||
let video_queue_map_cloned = video_queue_map.clone();
|
let video_queue_map_cloned = video_queue_map.clone();
|
||||||
let mut video_callback = video_callback;
|
let mut video_callback = video_callback;
|
||||||
|
|
||||||
let fps_map = Arc::new(RwLock::new(HashMap::new()));
|
let fps = Arc::new(RwLock::new(None));
|
||||||
let decode_fps_map = fps_map.clone();
|
let decode_fps_map = fps.clone();
|
||||||
let chroma = Arc::new(RwLock::new(None));
|
let chroma = Arc::new(RwLock::new(None));
|
||||||
let chroma_cloned = chroma.clone();
|
let chroma_cloned = chroma.clone();
|
||||||
let mut last_chroma = None;
|
let mut last_chroma = None;
|
||||||
@@ -2134,9 +2145,8 @@ where
|
|||||||
sync_cpu_usage();
|
sync_cpu_usage();
|
||||||
get_hwcodec_config();
|
get_hwcodec_config();
|
||||||
let mut handler_controller_map = HashMap::new();
|
let mut handler_controller_map = HashMap::new();
|
||||||
// let mut count = Vec::new();
|
let mut count = 0;
|
||||||
// let mut duration = std::time::Duration::ZERO;
|
let mut duration = std::time::Duration::ZERO;
|
||||||
// let mut skip_beginning = Vec::new();
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok(data) = video_receiver.recv() {
|
if let Ok(data) = video_receiver.recv() {
|
||||||
match data {
|
match data {
|
||||||
@@ -2169,8 +2179,6 @@ where
|
|||||||
display,
|
display,
|
||||||
VideoHandlerController {
|
VideoHandlerController {
|
||||||
handler: VideoHandler::new(format, display),
|
handler: VideoHandler::new(format, display),
|
||||||
count: 0,
|
|
||||||
duration: std::time::Duration::ZERO,
|
|
||||||
skip_beginning: 0,
|
skip_beginning: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -2178,6 +2186,8 @@ where
|
|||||||
if let Some(handler_controller) = handler_controller_map.get_mut(&display) {
|
if let Some(handler_controller) = handler_controller_map.get_mut(&display) {
|
||||||
let mut pixelbuffer = true;
|
let mut pixelbuffer = true;
|
||||||
let mut tmp_chroma = None;
|
let mut tmp_chroma = None;
|
||||||
|
let format_changed =
|
||||||
|
handler_controller.handler.decoder.format() != format;
|
||||||
match handler_controller.handler.handle_frame(
|
match handler_controller.handler.handle_frame(
|
||||||
vf,
|
vf,
|
||||||
&mut pixelbuffer,
|
&mut pixelbuffer,
|
||||||
@@ -2198,27 +2208,14 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fps calculation
|
// fps calculation
|
||||||
// The first frame will be very slow
|
fps_calculate(
|
||||||
if handler_controller.skip_beginning < 5 {
|
handler_controller,
|
||||||
handler_controller.skip_beginning += 1;
|
&fps,
|
||||||
continue;
|
format_changed,
|
||||||
}
|
start.elapsed(),
|
||||||
|
&mut count,
|
||||||
handler_controller.duration += start.elapsed();
|
&mut duration,
|
||||||
handler_controller.count += 1;
|
);
|
||||||
if handler_controller.count % 10 == 0 {
|
|
||||||
fps_map.write().unwrap().insert(
|
|
||||||
display,
|
|
||||||
(handler_controller.count * 1000
|
|
||||||
/ handler_controller.duration.as_millis())
|
|
||||||
as usize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Clear to get real-time fps
|
|
||||||
if handler_controller.count > 150 {
|
|
||||||
handler_controller.count = 0;
|
|
||||||
handler_controller.duration = Duration::ZERO;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// This is a simple workaround.
|
// This is a simple workaround.
|
||||||
@@ -2334,6 +2331,38 @@ pub fn start_audio_thread() -> MediaSender {
|
|||||||
audio_sender
|
audio_sender
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn fps_calculate(
|
||||||
|
handler_controller: &mut VideoHandlerController,
|
||||||
|
fps: &Arc<RwLock<Option<usize>>>,
|
||||||
|
format_changed: bool,
|
||||||
|
elapsed: std::time::Duration,
|
||||||
|
count: &mut usize,
|
||||||
|
duration: &mut std::time::Duration,
|
||||||
|
) {
|
||||||
|
if format_changed {
|
||||||
|
*count = 0;
|
||||||
|
*duration = std::time::Duration::ZERO;
|
||||||
|
handler_controller.skip_beginning = 0;
|
||||||
|
}
|
||||||
|
// // The first frame will be very slow
|
||||||
|
if handler_controller.skip_beginning < 3 {
|
||||||
|
handler_controller.skip_beginning += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*duration += elapsed;
|
||||||
|
*count += 1;
|
||||||
|
let ms = duration.as_millis();
|
||||||
|
if *count % 10 == 0 && ms > 0 {
|
||||||
|
*fps.write().unwrap() = Some((*count as usize) * 1000 / (ms as usize));
|
||||||
|
}
|
||||||
|
// Clear to get real-time fps
|
||||||
|
if *count >= 30 {
|
||||||
|
*count = 0;
|
||||||
|
*duration = Duration::ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_hwcodec_config() {
|
fn get_hwcodec_config() {
|
||||||
// for sciter and unilink
|
// for sciter and unilink
|
||||||
#[cfg(feature = "hwcodec")]
|
#[cfg(feature = "hwcodec")]
|
||||||
@@ -2596,7 +2625,7 @@ struct LoginErrorMsgBox {
|
|||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref LOGIN_ERROR_MAP: Arc<HashMap<&'static str, LoginErrorMsgBox>> = {
|
static ref LOGIN_ERROR_MAP: Arc<HashMap<&'static str, LoginErrorMsgBox>> = {
|
||||||
use hbb_common::config::LINK_HEADLESS_LINUX_SUPPORT;
|
use config::LINK_HEADLESS_LINUX_SUPPORT;
|
||||||
let map = HashMap::from([(LOGIN_SCREEN_WAYLAND, LoginErrorMsgBox{
|
let map = HashMap::from([(LOGIN_SCREEN_WAYLAND, LoginErrorMsgBox{
|
||||||
msgtype: "error",
|
msgtype: "error",
|
||||||
title: "Login Error",
|
title: "Login Error",
|
||||||
@@ -2762,6 +2791,20 @@ pub async fn handle_hash(
|
|||||||
if password.is_empty() {
|
if password.is_empty() {
|
||||||
try_get_password_from_personal_ab(lc.clone(), &mut password);
|
try_get_password_from_personal_ab(lc.clone(), &mut password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if password.is_empty() {
|
||||||
|
let p =
|
||||||
|
crate::ui_interface::get_buildin_option(config::keys::OPTION_DEFAULT_CONNECT_PASSWORD);
|
||||||
|
if !p.is_empty() {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(p.clone());
|
||||||
|
hasher.update(&hash.salt);
|
||||||
|
let res = hasher.finalize();
|
||||||
|
password = res[..].into();
|
||||||
|
lc.write().unwrap().password_source = PasswordSource::SharedAb(p); // reuse SharedAb here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lc.write().unwrap().password = password.clone();
|
lc.write().unwrap().password = password.clone();
|
||||||
let password = if password.is_empty() {
|
let password = if password.is_empty() {
|
||||||
// login without password, the remote side can click accept
|
// login without password, the remote side can click accept
|
||||||
@@ -2784,7 +2827,7 @@ pub async fn handle_hash(
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn try_get_password_from_personal_ab(lc: Arc<RwLock<LoginConfigHandler>>, password: &mut Vec<u8>) {
|
fn try_get_password_from_personal_ab(lc: Arc<RwLock<LoginConfigHandler>>, password: &mut Vec<u8>) {
|
||||||
let access_token = LocalConfig::get_option("access_token");
|
let access_token = LocalConfig::get_option("access_token");
|
||||||
let ab = hbb_common::config::Ab::load();
|
let ab = config::Ab::load();
|
||||||
if !access_token.is_empty() && access_token == ab.access_token {
|
if !access_token.is_empty() && access_token == ab.access_token {
|
||||||
let id = lc.read().unwrap().id.clone();
|
let id = lc.read().unwrap().id.clone();
|
||||||
if let Some(ab) = ab.ab_entries.iter().find(|a| a.personal()) {
|
if let Some(ab) = ab.ab_entries.iter().find(|a| a.personal()) {
|
||||||
@@ -3150,3 +3193,75 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b
|
|||||||
&& !text.to_lowercase().contains("manually")
|
&& !text.to_lowercase().contains("manually")
|
||||||
&& !text.to_lowercase().contains("not allowed")))
|
&& !text.to_lowercase().contains("not allowed")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn hc_connection(
|
||||||
|
feedback: i32,
|
||||||
|
rendezvous_server: String,
|
||||||
|
token: &str,
|
||||||
|
) -> Option<tokio::sync::mpsc::UnboundedSender<()>> {
|
||||||
|
if feedback == 0 || rendezvous_server.is_empty() || token.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (tx, rx) = unbounded_channel::<()>();
|
||||||
|
let token = token.to_owned();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
allow_err!(hc_connection_(rendezvous_server, rx, token).await);
|
||||||
|
});
|
||||||
|
Some(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn hc_connection_(
|
||||||
|
rendezvous_server: String,
|
||||||
|
mut rx: UnboundedReceiver<()>,
|
||||||
|
token: String,
|
||||||
|
) -> ResultType<()> {
|
||||||
|
let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT));
|
||||||
|
let mut last_recv_msg = Instant::now();
|
||||||
|
let mut keep_alive = crate::DEFAULT_KEEP_ALIVE;
|
||||||
|
|
||||||
|
let host = check_port(&rendezvous_server, RENDEZVOUS_PORT);
|
||||||
|
let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?;
|
||||||
|
let key = crate::get_key(true).await;
|
||||||
|
crate::secure_tcp(&mut conn, &key).await?;
|
||||||
|
let mut msg_out = RendezvousMessage::new();
|
||||||
|
msg_out.set_hc(HealthCheck {
|
||||||
|
token,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
conn.send(&msg_out).await?;
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
res = rx.recv() => {
|
||||||
|
if res.is_none() {
|
||||||
|
log::debug!("HC connection is closed as controlling connection exits");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = conn.next() => {
|
||||||
|
last_recv_msg = Instant::now();
|
||||||
|
let bytes = res.ok_or_else(|| anyhow!("Rendezvous connection is reset by the peer"))??;
|
||||||
|
if bytes.is_empty() {
|
||||||
|
conn.send_bytes(bytes::Bytes::new()).await?;
|
||||||
|
continue; // heartbeat
|
||||||
|
}
|
||||||
|
let msg = RendezvousMessage::parse_from_bytes(&bytes)?;
|
||||||
|
match msg.union {
|
||||||
|
Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => {
|
||||||
|
if rpr.keep_alive > 0 {
|
||||||
|
keep_alive = rpr.keep_alive * 1000;
|
||||||
|
log::info!("keep_alive: {}ms", keep_alive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = timer.tick() => {
|
||||||
|
// https://www.emqx.com/en/blog/mqtt-keep-alive
|
||||||
|
if last_recv_msg.elapsed().as_millis() as u64 > keep_alive as u64 * 3 / 2 {
|
||||||
|
bail!("HC connection is timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType};
|
|||||||
use scrap::CodecFormat;
|
use scrap::CodecFormat;
|
||||||
|
|
||||||
use crate::client::{
|
use crate::client::{
|
||||||
new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
|
self, new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
|
||||||
};
|
};
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
use crate::common::{self, update_clipboard};
|
use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL};
|
||||||
use crate::common::{get_default_sound_input, set_sound_input};
|
use crate::common::get_default_sound_input;
|
||||||
use crate::ui_session_interface::{InvokeUiSession, Session};
|
use crate::ui_session_interface::{InvokeUiSession, Session};
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "ios")))]
|
||||||
use crate::{audio_service, ConnInner, CLIENT_SERVER};
|
use crate::{audio_service, ConnInner, CLIENT_SERVER};
|
||||||
@@ -71,8 +71,8 @@ pub struct Remote<T: InvokeUiSession> {
|
|||||||
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
|
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
|
||||||
video_format: CodecFormat,
|
video_format: CodecFormat,
|
||||||
elevation_requested: bool,
|
elevation_requested: bool,
|
||||||
fps_control_map: HashMap<usize, FpsControl>,
|
fps_control: FpsControl,
|
||||||
decode_fps_map: Arc<RwLock<HashMap<usize, usize>>>,
|
decode_fps: Arc<RwLock<Option<usize>>>,
|
||||||
chroma: Arc<RwLock<Option<Chroma>>>,
|
chroma: Arc<RwLock<Option<Chroma>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
receiver: mpsc::UnboundedReceiver<Data>,
|
receiver: mpsc::UnboundedReceiver<Data>,
|
||||||
sender: mpsc::UnboundedSender<Data>,
|
sender: mpsc::UnboundedSender<Data>,
|
||||||
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
|
frame_count_map: Arc<RwLock<HashMap<usize, usize>>>,
|
||||||
decode_fps: Arc<RwLock<HashMap<usize, usize>>>,
|
decode_fps: Arc<RwLock<Option<usize>>>,
|
||||||
chroma: Arc<RwLock<Option<Chroma>>>,
|
chroma: Arc<RwLock<Option<Chroma>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -110,8 +110,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
stop_voice_call_sender: None,
|
stop_voice_call_sender: None,
|
||||||
voice_call_request_timestamp: None,
|
voice_call_request_timestamp: None,
|
||||||
elevation_requested: false,
|
elevation_requested: false,
|
||||||
fps_control_map: Default::default(),
|
fps_control: Default::default(),
|
||||||
decode_fps_map: decode_fps,
|
decode_fps,
|
||||||
chroma,
|
chroma,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((mut peer, direct, pk)) => {
|
Ok(((mut peer, direct, pk), (feedback, rendezvous_server))) => {
|
||||||
self.handler
|
self.handler
|
||||||
.connection_round_state
|
.connection_round_state
|
||||||
.lock()
|
.lock()
|
||||||
@@ -173,6 +173,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
crate::rustdesk_interval(time::interval(Duration::new(1, 0)));
|
crate::rustdesk_interval(time::interval(Duration::new(1, 0)));
|
||||||
let mut fps_instant = Instant::now();
|
let mut fps_instant = Instant::now();
|
||||||
|
|
||||||
|
let _keep_it =
|
||||||
|
client::hc_connection(feedback, rendezvous_server, token).await;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = peer.next() => {
|
res = peer.next() => {
|
||||||
@@ -387,14 +390,15 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
if self.handler.is_file_transfer() || self.handler.is_port_forward() {
|
if self.handler.is_file_transfer() || self.handler.is_port_forward() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Switch to default input device
|
|
||||||
let default_sound_device = get_default_sound_input();
|
|
||||||
if let Some(device) = default_sound_device {
|
|
||||||
set_sound_input(device);
|
|
||||||
}
|
|
||||||
// iOS does not have this server.
|
// iOS does not have this server.
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "ios")))]
|
||||||
{
|
{
|
||||||
|
// NOTE:
|
||||||
|
// The client server and --server both use the same sound input device.
|
||||||
|
// It's better to distinguish the server side and client side.
|
||||||
|
// But it' not necessary for now, because it's not a common case.
|
||||||
|
// And it is immediately known when the input device is changed.
|
||||||
|
crate::audio_service::set_voice_call_input_device(get_default_sound_input(), false);
|
||||||
// Create a channel to receive error or closed message
|
// Create a channel to receive error or closed message
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
let (tx_audio_data, mut rx_audio_data) =
|
let (tx_audio_data, mut rx_audio_data) =
|
||||||
@@ -421,6 +425,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
client_conn_inner,
|
client_conn_inner,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
crate::audio_service::set_voice_call_input_device(None, true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -971,69 +976,85 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
if custom_fps < 5 || custom_fps > 120 {
|
if custom_fps < 5 || custom_fps > 120 {
|
||||||
custom_fps = 30;
|
custom_fps = 30;
|
||||||
}
|
}
|
||||||
let decode_fps_read = self.decode_fps_map.read().unwrap();
|
let ctl = &mut self.fps_control;
|
||||||
for (display, decode_fps) in decode_fps_read.iter() {
|
let len = self
|
||||||
let video_queue_map_read = self.video_queue_map.read().unwrap();
|
.video_queue_map
|
||||||
let Some(video_queue) = video_queue_map_read.get(display) else {
|
.read()
|
||||||
continue;
|
.unwrap()
|
||||||
};
|
.iter()
|
||||||
|
.map(|v| v.1.len())
|
||||||
if !self.fps_control_map.contains_key(display) {
|
.max()
|
||||||
self.fps_control_map.insert(*display, FpsControl::default());
|
.unwrap_or_default();
|
||||||
|
let decode_fps = self.decode_fps.read().unwrap().clone();
|
||||||
|
let Some(mut decode_fps) = decode_fps else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if cfg!(feature = "flutter") {
|
||||||
|
let active_displays = ctl
|
||||||
|
.last_active_time
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.1.elapsed().as_secs() < 5)
|
||||||
|
.count();
|
||||||
|
if active_displays > 1 {
|
||||||
|
decode_fps = decode_fps / active_displays;
|
||||||
}
|
}
|
||||||
let Some(ctl) = self.fps_control_map.get_mut(display) else {
|
}
|
||||||
return;
|
let mut limited_fps = if direct {
|
||||||
};
|
decode_fps * 9 / 10 // 30 got 27
|
||||||
|
} else {
|
||||||
|
decode_fps * 4 / 5 // 30 got 24
|
||||||
|
};
|
||||||
|
if limited_fps > custom_fps {
|
||||||
|
limited_fps = custom_fps;
|
||||||
|
}
|
||||||
|
let last_auto_fps = self.handler.lc.read().unwrap().last_auto_fps.clone();
|
||||||
|
let should_decrease = (len > 1
|
||||||
|
&& last_auto_fps.clone().unwrap_or(custom_fps as _) > limited_fps)
|
||||||
|
|| len > std::cmp::max(1, limited_fps / 2);
|
||||||
|
|
||||||
let len = video_queue.len();
|
// increase judgement
|
||||||
let decode_fps = *decode_fps;
|
if len <= 1 {
|
||||||
let mut limited_fps = if direct {
|
if ctl.idle_counter < usize::MAX {
|
||||||
decode_fps * 9 / 10 // 30 got 27
|
|
||||||
} else {
|
|
||||||
decode_fps * 4 / 5 // 30 got 24
|
|
||||||
};
|
|
||||||
if limited_fps > custom_fps {
|
|
||||||
limited_fps = custom_fps;
|
|
||||||
}
|
|
||||||
let should_decrease = len > 1 && ctl.last_auto_fps.unwrap_or(0) > limited_fps as i32;
|
|
||||||
|
|
||||||
// increase judgement
|
|
||||||
if len <= 1 {
|
|
||||||
ctl.idle_counter += 1;
|
ctl.idle_counter += 1;
|
||||||
} else {
|
|
||||||
ctl.idle_counter = 0;
|
|
||||||
}
|
}
|
||||||
let mut should_increase = false;
|
} else {
|
||||||
if let Some(last_auto_fps) = ctl.last_auto_fps {
|
ctl.idle_counter = 0;
|
||||||
// ever set
|
}
|
||||||
if last_auto_fps + 3 <= limited_fps as i32 && ctl.idle_counter > 3 {
|
let mut should_increase = false;
|
||||||
// limited_fps is 5 larger than last set, and idle time is more than 3 seconds
|
if let Some(last_auto_fps) = last_auto_fps.clone() {
|
||||||
should_increase = true;
|
// ever set
|
||||||
}
|
if last_auto_fps + 3 <= limited_fps && ctl.idle_counter > 3 {
|
||||||
|
// limited_fps is 3 larger than last set, and idle time is more than 3 seconds
|
||||||
|
should_increase = true;
|
||||||
}
|
}
|
||||||
if ctl.last_auto_fps.is_none() || should_decrease || should_increase {
|
}
|
||||||
// limited_fps to ensure decoding is faster than encoding
|
if last_auto_fps.is_none() || should_decrease || should_increase {
|
||||||
let mut auto_fps = limited_fps as i32;
|
// limited_fps to ensure decoding is faster than encoding
|
||||||
if auto_fps < 1 {
|
let mut auto_fps = limited_fps;
|
||||||
auto_fps = 1;
|
if should_decrease && limited_fps < len {
|
||||||
}
|
auto_fps = limited_fps / 2;
|
||||||
// send custom fps
|
|
||||||
let mut misc = Misc::new();
|
|
||||||
misc.set_option(OptionMessage {
|
|
||||||
custom_fps: auto_fps,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let mut msg = Message::new();
|
|
||||||
msg.set_misc(misc);
|
|
||||||
self.sender.send(Data::Message(msg)).ok();
|
|
||||||
ctl.last_queue_size = len;
|
|
||||||
ctl.last_auto_fps = Some(auto_fps);
|
|
||||||
}
|
}
|
||||||
// send refresh
|
if auto_fps < 1 {
|
||||||
|
auto_fps = 1;
|
||||||
|
}
|
||||||
|
let mut misc = Misc::new();
|
||||||
|
misc.set_option(OptionMessage {
|
||||||
|
custom_fps: auto_fps as _,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let mut msg = Message::new();
|
||||||
|
msg.set_misc(misc);
|
||||||
|
self.sender.send(Data::Message(msg)).ok();
|
||||||
|
log::info!("Set fps to {}", auto_fps);
|
||||||
|
ctl.last_queue_size = len;
|
||||||
|
self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps);
|
||||||
|
}
|
||||||
|
// send refresh
|
||||||
|
for (display, video_queue) in self.video_queue_map.read().unwrap().iter() {
|
||||||
let tolerable = std::cmp::min(decode_fps, video_queue.capacity() / 2);
|
let tolerable = std::cmp::min(decode_fps, video_queue.capacity() / 2);
|
||||||
if ctl.refresh_times < 10 // enough
|
if ctl.refresh_times < 20 // enough
|
||||||
&& (len > tolerable
|
&& (video_queue.len() > tolerable
|
||||||
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 10))
|
&& (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 10))
|
||||||
{
|
{
|
||||||
// Refresh causes client set_display, left frames cause flickering.
|
// Refresh causes client set_display, left frames cause flickering.
|
||||||
while let Some(_) = video_queue.pop() {}
|
while let Some(_) = video_queue.pop() {}
|
||||||
@@ -1086,6 +1107,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
self.video_sender.send(MediaData::VideoQueue(display)).ok();
|
self.video_sender.send(MediaData::VideoQueue(display)).ok();
|
||||||
}
|
}
|
||||||
|
self.fps_control
|
||||||
|
.last_active_time
|
||||||
|
.insert(display, Instant::now());
|
||||||
}
|
}
|
||||||
Some(message::Union::Hash(hash)) => {
|
Some(message::Union::Hash(hash)) => {
|
||||||
self.handler
|
self.handler
|
||||||
@@ -1116,16 +1140,16 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
// To make sure current text clipboard data is updated.
|
// To make sure current text clipboard data is updated.
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if let Some(mut rx) = rx {
|
if let Some(mut rx) = rx {
|
||||||
timeout(common::CLIPBOARD_INTERVAL, rx.recv()).await.ok();
|
timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if let Some(msg_out) = Client::get_current_text_clipboard_msg() {
|
if let Some(msg_out) = Client::get_current_clipboard_msg() {
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
let permission_config = self.handler.get_permission_config();
|
let permission_config = self.handler.get_permission_config();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// due to clipboard service interval time
|
// due to clipboard service interval time
|
||||||
sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await;
|
sleep(CLIPBOARD_INTERVAL as f32 / 1_000.).await;
|
||||||
if permission_config.is_text_clipboard_required() {
|
if permission_config.is_text_clipboard_required() {
|
||||||
sender.send(Data::Message(msg_out)).ok();
|
sender.send(Data::Message(msg_out)).ok();
|
||||||
}
|
}
|
||||||
@@ -1161,7 +1185,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
Some(message::Union::Clipboard(cb)) => {
|
Some(message::Union::Clipboard(cb)) => {
|
||||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
update_clipboard(cb, Some(&crate::client::get_old_clipboard_text()));
|
update_clipboard(cb, Some(crate::client::get_old_clipboard_text()));
|
||||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
{
|
{
|
||||||
let content = if cb.compress {
|
let content = if cb.compress {
|
||||||
@@ -1840,8 +1864,8 @@ struct FpsControl {
|
|||||||
last_queue_size: usize,
|
last_queue_size: usize,
|
||||||
refresh_times: usize,
|
refresh_times: usize,
|
||||||
last_refresh_instant: Instant,
|
last_refresh_instant: Instant,
|
||||||
last_auto_fps: Option<i32>,
|
|
||||||
idle_counter: usize,
|
idle_counter: usize,
|
||||||
|
last_active_time: HashMap<usize, Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FpsControl {
|
impl Default for FpsControl {
|
||||||
@@ -1850,8 +1874,8 @@ impl Default for FpsControl {
|
|||||||
last_queue_size: Default::default(),
|
last_queue_size: Default::default(),
|
||||||
refresh_times: Default::default(),
|
refresh_times: Default::default(),
|
||||||
last_refresh_instant: Instant::now(),
|
last_refresh_instant: Instant::now(),
|
||||||
last_auto_fps: None,
|
|
||||||
idle_counter: 0,
|
idle_counter: 0,
|
||||||
|
last_active_time: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
444
src/clipboard.rs
Normal file
444
src/clipboard.rs
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
Arc, Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown};
|
||||||
|
use hbb_common::{
|
||||||
|
allow_err,
|
||||||
|
compress::{compress as compress_func, decompress},
|
||||||
|
log,
|
||||||
|
message_proto::*,
|
||||||
|
ResultType,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CLIPBOARD_NAME: &'static str = "clipboard";
|
||||||
|
pub const CLIPBOARD_INTERVAL: u64 = 333;
|
||||||
|
const FAKE_SVG_WIDTH: usize = 999999;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
pub static ref CONTENT: Arc<Mutex<ClipboardData>> = Default::default();
|
||||||
|
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||||
|
static X11_CLIPBOARD: once_cell::sync::OnceCell<x11_clipboard::Clipboard> =
|
||||||
|
once_cell::sync::OnceCell::new();
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||||
|
fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> {
|
||||||
|
X11_CLIPBOARD
|
||||||
|
.get_or_try_init(|| x11_clipboard::Clipboard::new())
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||||
|
pub struct ClipboardContext {
|
||||||
|
string_setter: x11rb::protocol::xproto::Atom,
|
||||||
|
string_getter: x11rb::protocol::xproto::Atom,
|
||||||
|
text_uri_list: x11rb::protocol::xproto::Atom,
|
||||||
|
|
||||||
|
clip: x11rb::protocol::xproto::Atom,
|
||||||
|
prop: x11rb::protocol::xproto::Atom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||||
|
fn parse_plain_uri_list(v: Vec<u8>) -> Result<String, String> {
|
||||||
|
let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?;
|
||||||
|
let mut list = String::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
if !line.starts_with("file://") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let decoded = percent_encoding::percent_decode_str(line)
|
||||||
|
.decode_utf8()
|
||||||
|
.map_err(|_| "ConversionFailure".to_owned())?;
|
||||||
|
list = list + "\n" + decoded.trim_start_matches("file://");
|
||||||
|
}
|
||||||
|
list = list.trim().to_owned();
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||||
|
impl ClipboardContext {
|
||||||
|
pub fn new(_listen: bool) -> Result<Self, String> {
|
||||||
|
let clipboard = get_clipboard()?;
|
||||||
|
let string_getter = clipboard
|
||||||
|
.getter
|
||||||
|
.get_atom("UTF8_STRING")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let string_setter = clipboard
|
||||||
|
.setter
|
||||||
|
.get_atom("UTF8_STRING")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let text_uri_list = clipboard
|
||||||
|
.getter
|
||||||
|
.get_atom("text/uri-list")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let prop = clipboard.getter.atoms.property;
|
||||||
|
let clip = clipboard.getter.atoms.clipboard;
|
||||||
|
Ok(Self {
|
||||||
|
text_uri_list,
|
||||||
|
string_setter,
|
||||||
|
string_getter,
|
||||||
|
clip,
|
||||||
|
prop,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text(&mut self) -> Result<String, String> {
|
||||||
|
let clip = self.clip;
|
||||||
|
let prop = self.prop;
|
||||||
|
|
||||||
|
const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120);
|
||||||
|
|
||||||
|
let text_content = get_clipboard()?
|
||||||
|
.load(clip, self.string_getter, prop, TIMEOUT)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let file_urls = get_clipboard()?.load(clip, self.text_uri_list, prop, TIMEOUT)?;
|
||||||
|
|
||||||
|
if file_urls.is_err() || file_urls.as_ref().is_empty() {
|
||||||
|
log::trace!("clipboard get text, no file urls");
|
||||||
|
return String::from_utf8(text_content).map_err(|e| e.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_urls = parse_plain_uri_list(file_urls)?;
|
||||||
|
|
||||||
|
let text_content = String::from_utf8(text_content).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if text_content.trim() == file_urls.trim() {
|
||||||
|
log::trace!("clipboard got text but polluted");
|
||||||
|
return Err(String::from("polluted text"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(text_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, content: String) -> Result<(), String> {
|
||||||
|
let clip = self.clip;
|
||||||
|
|
||||||
|
let value = content.clone().into_bytes();
|
||||||
|
get_clipboard()?
|
||||||
|
.store(clip, self.string_setter, value)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_clipboard(
|
||||||
|
ctx: &mut Option<ClipboardContext>,
|
||||||
|
old: Option<Arc<Mutex<ClipboardData>>>,
|
||||||
|
) -> Option<Message> {
|
||||||
|
if ctx.is_none() {
|
||||||
|
*ctx = ClipboardContext::new(true).ok();
|
||||||
|
}
|
||||||
|
let ctx2 = ctx.as_mut()?;
|
||||||
|
let side = if old.is_none() { "host" } else { "client" };
|
||||||
|
let old = if let Some(old) = old {
|
||||||
|
old
|
||||||
|
} else {
|
||||||
|
CONTENT.clone()
|
||||||
|
};
|
||||||
|
let content = ctx2.get();
|
||||||
|
if let Ok(content) = content {
|
||||||
|
if !content.is_empty() {
|
||||||
|
if matches!(content, ClipboardData::Text(_)) {
|
||||||
|
// Skip the text if the last content is image-svg/html
|
||||||
|
if ctx2.is_last_plain {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = content != *old.lock().unwrap();
|
||||||
|
if changed {
|
||||||
|
log::info!("{} update found on {}", CLIPBOARD_NAME, side);
|
||||||
|
let msg = content.create_msg();
|
||||||
|
*old.lock().unwrap() = content;
|
||||||
|
return Some(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_clipboard_(clipboard: Clipboard, old: Option<Arc<Mutex<ClipboardData>>>) {
|
||||||
|
let content = ClipboardData::from_msg(clipboard);
|
||||||
|
if content.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match ClipboardContext::new(false) {
|
||||||
|
Ok(mut ctx) => {
|
||||||
|
let side = if old.is_none() { "host" } else { "client" };
|
||||||
|
let old = if let Some(old) = old {
|
||||||
|
old
|
||||||
|
} else {
|
||||||
|
CONTENT.clone()
|
||||||
|
};
|
||||||
|
allow_err!(ctx.set(&content));
|
||||||
|
*old.lock().unwrap() = content;
|
||||||
|
log::debug!("{} updated on {}", CLIPBOARD_NAME, side);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to create clipboard context: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_clipboard(clipboard: Clipboard, old: Option<Arc<Mutex<ClipboardData>>>) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
update_clipboard_(clipboard, old);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ClipboardData {
|
||||||
|
Text(String),
|
||||||
|
Image(arboard::ImageData<'static>, u64),
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ClipboardData {
|
||||||
|
fn default() -> Self {
|
||||||
|
ClipboardData::Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClipboardData {
|
||||||
|
fn image(image: arboard::ImageData<'static>) -> ClipboardData {
|
||||||
|
let hash = 0;
|
||||||
|
/*
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
image.bytes.hash(&mut hasher);
|
||||||
|
let hash = hasher.finish();
|
||||||
|
*/
|
||||||
|
ClipboardData::Image(image, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ClipboardData::Empty => true,
|
||||||
|
ClipboardData::Text(s) => s.is_empty(),
|
||||||
|
ClipboardData::Image(a, _) => a.bytes().is_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_msg(clipboard: Clipboard) -> Self {
|
||||||
|
let is_image = clipboard.width > 0;
|
||||||
|
let data = if clipboard.compress {
|
||||||
|
decompress(&clipboard.content)
|
||||||
|
} else {
|
||||||
|
clipboard.content.into()
|
||||||
|
};
|
||||||
|
if is_image {
|
||||||
|
// We cannot use data.start_with(b"<svg") to check if it is svg image
|
||||||
|
// because svg image may starts with other bytes
|
||||||
|
let img = if clipboard.height == 0 && clipboard.width as usize == FAKE_SVG_WIDTH {
|
||||||
|
arboard::ImageData::svg(std::str::from_utf8(&data).unwrap_or_default())
|
||||||
|
} else {
|
||||||
|
arboard::ImageData::rgba(clipboard.width as _, clipboard.height as _, data.into())
|
||||||
|
};
|
||||||
|
ClipboardData::Image(img, 0)
|
||||||
|
} else {
|
||||||
|
if let Ok(content) = String::from_utf8(data) {
|
||||||
|
ClipboardData::Text(content)
|
||||||
|
} else {
|
||||||
|
ClipboardData::Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_msg(&self) -> Message {
|
||||||
|
let mut msg = Message::new();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
ClipboardData::Text(s) => {
|
||||||
|
let compressed = compress_func(s.as_bytes());
|
||||||
|
let compress = compressed.len() < s.as_bytes().len();
|
||||||
|
let content = if compress {
|
||||||
|
compressed
|
||||||
|
} else {
|
||||||
|
s.clone().into_bytes()
|
||||||
|
};
|
||||||
|
msg.set_clipboard(Clipboard {
|
||||||
|
compress,
|
||||||
|
content: content.into(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ClipboardData::Image(a, _) => {
|
||||||
|
let compressed = compress_func(&a.bytes());
|
||||||
|
let compress = compressed.len() < a.bytes().len();
|
||||||
|
let content = if compress {
|
||||||
|
compressed
|
||||||
|
} else {
|
||||||
|
a.bytes().to_vec()
|
||||||
|
};
|
||||||
|
let (w, h) = match a {
|
||||||
|
arboard::ImageData::Rgba(a) => (a.width, a.height),
|
||||||
|
arboard::ImageData::Svg(_) => (FAKE_SVG_WIDTH as _, 0 as _),
|
||||||
|
};
|
||||||
|
msg.set_clipboard(Clipboard {
|
||||||
|
compress,
|
||||||
|
content: content.into(),
|
||||||
|
width: w as _,
|
||||||
|
height: h as _,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ClipboardData {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(ClipboardData::Text(a), ClipboardData::Text(b)) => a == b,
|
||||||
|
(ClipboardData::Image(a, _), ClipboardData::Image(b, _)) => match (a, b) {
|
||||||
|
(arboard::ImageData::Rgba(a), arboard::ImageData::Rgba(b)) => {
|
||||||
|
a.width == b.width && a.height == b.height && a.bytes == b.bytes
|
||||||
|
}
|
||||||
|
(arboard::ImageData::Svg(a), arboard::ImageData::Svg(b)) => a == b,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
(ClipboardData::Empty, ClipboardData::Empty) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
|
||||||
|
pub struct ClipboardContext {
|
||||||
|
inner: arboard::Clipboard,
|
||||||
|
counter: (Arc<AtomicU64>, u64),
|
||||||
|
shutdown: Option<Shutdown>,
|
||||||
|
is_last_plain: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
impl ClipboardContext {
|
||||||
|
pub fn new(listen: bool) -> ResultType<ClipboardContext> {
|
||||||
|
let board;
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
board = arboard::Clipboard::new()?;
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let mut i = 1;
|
||||||
|
loop {
|
||||||
|
// Try 5 times to create clipboard
|
||||||
|
// Arboard::new() connect to X server or Wayland compositor, which shoud be ok at most time
|
||||||
|
// But sometimes, the connection may fail, so we retry here.
|
||||||
|
match arboard::Clipboard::new() {
|
||||||
|
Ok(x) => {
|
||||||
|
board = x;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if i == 5 {
|
||||||
|
return Err(e.into());
|
||||||
|
} else {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(30 * i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// starting from 1 so that we can always get initial clipboard data no matter if change
|
||||||
|
let change_count: Arc<AtomicU64> = Arc::new(AtomicU64::new(1));
|
||||||
|
let mut shutdown = None;
|
||||||
|
if listen {
|
||||||
|
struct Handler(Arc<AtomicU64>);
|
||||||
|
impl ClipboardHandler for Handler {
|
||||||
|
fn on_clipboard_change(&mut self) -> CallbackResult {
|
||||||
|
self.0.fetch_add(1, Ordering::SeqCst);
|
||||||
|
CallbackResult::Next
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_clipboard_error(&mut self, error: std::io::Error) -> CallbackResult {
|
||||||
|
log::trace!("Error of clipboard listener: {}", error);
|
||||||
|
CallbackResult::Next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let change_count_cloned = change_count.clone();
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread.
|
||||||
|
std::thread::spawn(move || match Master::new(Handler(change_count_cloned)) {
|
||||||
|
Ok(mut master) => {
|
||||||
|
tx.send(master.shutdown_channel()).ok();
|
||||||
|
log::debug!("Clipboard listener started");
|
||||||
|
if let Err(err) = master.run() {
|
||||||
|
log::error!("Failed to run clipboard listener: {}", err);
|
||||||
|
} else {
|
||||||
|
log::debug!("Clipboard listener stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to create clipboard listener: {}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Ok(st) = rx.recv() {
|
||||||
|
shutdown = Some(st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ClipboardContext {
|
||||||
|
inner: board,
|
||||||
|
counter: (change_count, 0),
|
||||||
|
shutdown,
|
||||||
|
is_last_plain: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn change_count(&self) -> u64 {
|
||||||
|
debug_assert!(self.shutdown.is_some());
|
||||||
|
self.counter.0.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&mut self) -> ResultType<ClipboardData> {
|
||||||
|
let cn = self.change_count();
|
||||||
|
let _lock = ARBOARD_MTX.lock().unwrap();
|
||||||
|
// only for image for the time being,
|
||||||
|
// because I do not want to change behavior of text clipboard for the time being
|
||||||
|
if cn != self.counter.1 {
|
||||||
|
self.is_last_plain = false;
|
||||||
|
self.counter.1 = cn;
|
||||||
|
if let Ok(image) = self.inner.get_image() {
|
||||||
|
// Both text and image svg may be set by some applications
|
||||||
|
// But we only want to send the svg content.
|
||||||
|
//
|
||||||
|
// We can't call `get_text()` and store current text in `old` in outer scope,
|
||||||
|
// because it may be updated later than svg.
|
||||||
|
// Then the text will still be sent and replace the image svg content.
|
||||||
|
self.is_last_plain = matches!(image, arboard::ImageData::Svg(_));
|
||||||
|
return Ok(ClipboardData::image(image));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ClipboardData::Text(self.inner.get_text()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, data: &ClipboardData) -> ResultType<()> {
|
||||||
|
let _lock = ARBOARD_MTX.lock().unwrap();
|
||||||
|
match data {
|
||||||
|
ClipboardData::Text(s) => self.inner.set_text(s)?,
|
||||||
|
ClipboardData::Image(a, _) => self.inner.set_image(a.clone())?,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ClipboardContext {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(shutdown) = self.shutdown.take() {
|
||||||
|
let _ = shutdown.signal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user